后端新增 OssUtils/OssTokenVo/OssCorsInitRunner,通过 STS 临时凭证实现客户端直传 OSS; 前端 upload API 适配直传流程,赛事创建/作品提交/作业/富文本编辑器均已切换; 多环境(dev/test/prod) OSS 配置补全;新增 oss-direct-upload-demo 示例项目及 E2E 测试。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
12 KiB
Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
Test info
- Name: upload\oss-upload.spec.ts >> OSS 直传上传 >> 登录 -> 赛事创建页 -> 上传封面图片到 OSS
- Location: e2e\upload\oss-upload.spec.ts:33:3
Error details
Error: expect(received).not.toContain(expected) // indexOf
Expected substring: not "/login"
Received string: "http://localhost:3000/super/login"
Page snapshot
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- generic [ref=e7]:
- generic [ref=e8]:
- img "乐绘世界" [ref=e9]
- generic [ref=e10]:
- generic [ref=e11]: 乐绘世界
- generic [ref=e12]: 创想活动乐园
- menu [ref=e13]:
- generic [ref=e14] [cursor=pointer]:
- img "fund-view" [ref=e15]:
- img [ref=e16]
- generic [ref=e20]: 活动监管
- list [ref=e21]:
- menuitem "unordered-list 全部活动" [ref=e22] [cursor=pointer]:
- img "unordered-list" [ref=e23]:
- img [ref=e24]
- generic [ref=e26]: 全部活动
- menuitem "user-add 报名数据" [ref=e27] [cursor=pointer]:
- img "user-add" [ref=e28]:
- img [ref=e29]
- generic [ref=e31]: 报名数据
- menuitem "file-text 作品数据" [ref=e32] [cursor=pointer]:
- img "file-text" [ref=e33]:
- img [ref=e34]
- generic [ref=e36]: 作品数据
- menuitem "dashboard 评审进度" [ref=e37] [cursor=pointer]:
- img "dashboard" [ref=e38]:
- img [ref=e39]
- generic [ref=e41]: 评审进度
- menuitem "trophy 活动成果" [ref=e42] [cursor=pointer]:
- img "trophy" [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: 活动成果
- generic [ref=e47] [cursor=pointer]:
- img "picture" [ref=e48]:
- img [ref=e49]
- generic [ref=e51]: 内容管理
- menuitem "bank 机构管理" [ref=e52] [cursor=pointer]:
- img "bank" [ref=e53]:
- img [ref=e54]
- generic [ref=e56]: 机构管理
- generic [ref=e57] [cursor=pointer]:
- img "team" [ref=e58]:
- img [ref=e59]
- generic [ref=e61]: 用户中心
- generic [ref=e62] [cursor=pointer]:
- img "setting" [ref=e63]:
- img [ref=e64]
- generic [ref=e66]: 系统设置
- generic [ref=e67]:
- generic [ref=e68] [cursor=pointer]:
- img [ref=e70]
- generic [ref=e71]: 超级管理员
- img "menu-fold" [ref=e73] [cursor=pointer]:
- img [ref=e74]
- main [ref=e77]:
- generic [ref=e78]:
- generic [ref=e82]: 活动列表
- generic [ref=e83]:
- generic [ref=e84] [cursor=pointer]:
- img "appstore" [ref=e86]:
- img [ref=e87]
- generic [ref=e89]:
- generic [ref=e90]: "0"
- generic [ref=e91]: 全部
- generic [ref=e92] [cursor=pointer]:
- img "form" [ref=e94]:
- img [ref=e95]
- generic [ref=e98]:
- generic [ref=e99]: "0"
- generic [ref=e100]: 报名中
- generic [ref=e101] [cursor=pointer]:
- img "edit" [ref=e103]:
- img [ref=e104]
- generic [ref=e106]:
- generic [ref=e107]: "0"
- generic [ref=e108]: 征稿中
- generic [ref=e109] [cursor=pointer]:
- img "eye" [ref=e111]:
- img [ref=e112]
- generic [ref=e114]:
- generic [ref=e115]: "0"
- generic [ref=e116]: 评审中
- generic [ref=e117] [cursor=pointer]:
- img "check-circle" [ref=e119]:
- img [ref=e120]
- generic [ref=e123]:
- generic [ref=e124]: "0"
- generic [ref=e125]: 已结束
- generic [ref=e126] [cursor=pointer]:
- img "close-circle" [ref=e128]:
- img [ref=e129]
- generic [ref=e131]:
- generic [ref=e132]: "0"
- generic [ref=e133]: 未发布
- generic [ref=e134]:
- generic [ref=e136]:
- generic "活动名称" [ref=e138]: "活动名称 :"
- textbox "请输入活动名称" [ref=e143]
- generic [ref=e146]:
- generic "活动阶段" [ref=e148]: "活动阶段 :"
- generic [ref=e152] [cursor=pointer]:
- generic [ref=e153]:
- combobox [ref=e155]
- generic: 全部阶段
- generic:
- img:
- img
- generic [ref=e157]:
- generic "活动类型" [ref=e159]: "活动类型 :"
- generic [ref=e163] [cursor=pointer]:
- generic [ref=e164]:
- combobox [ref=e166]
- generic: 全部
- generic:
- img:
- img
- generic [ref=e168]:
- generic "主办机构" [ref=e170]: "主办机构 :"
- generic [ref=e174] [cursor=pointer]:
- generic [ref=e175]:
- combobox [ref=e177]
- generic: 全部机构
- generic:
- img:
- img
- generic [ref=e182]:
- button "search 搜索" [ref=e183] [cursor=pointer]:
- img "search" [ref=e184]:
- img [ref=e185]
- generic [ref=e187]: 搜索
- button "reload 重置" [ref=e188] [cursor=pointer]:
- img "reload" [ref=e189]:
- img [ref=e190]
- generic [ref=e192]: 重置
- table [ref=e199]:
- rowgroup [ref=e212]:
- row "序号 活动名称 主办机构 类型 阶段 可见范围 报名 作品 评审 活动时间 操作" [ref=e213]:
- columnheader "序号" [ref=e214]
- columnheader "活动名称" [ref=e215]
- columnheader "主办机构" [ref=e216]
- columnheader "类型" [ref=e217]
- columnheader "阶段" [ref=e218]
- columnheader "可见范围" [ref=e219]
- columnheader "报名" [ref=e220]
- columnheader "作品" [ref=e221]
- columnheader "评审" [ref=e222]
- columnheader "活动时间" [ref=e223]
- columnheader "操作" [ref=e224]
- rowgroup [ref=e225]:
- row "暂无数据" [ref=e226]:
- cell "暂无数据" [ref=e227]:
- generic [ref=e228]:
- img [ref=e230]
- paragraph [ref=e236]: 暂无数据
Test source
1 | import { test, expect } from '@playwright/test'
2 | import path from 'path'
3 | import { fileURLToPath } from 'url'
4 | import fs from 'fs'
5 |
6 | const __filename = fileURLToPath(import.meta.url)
7 | const __dirname = path.dirname(__filename)
8 |
9 | // 测试配置
10 | const TENANT_CODE = 'super'
11 | const USERNAME = 'admin'
12 | const PASSWORD = 'admin123'
13 |
14 | // 确保测试图片存在
15 | const FIXTURES_DIR = path.join(__dirname, 'fixtures')
16 | const TEST_IMAGE_PATH = path.join(FIXTURES_DIR, 'test-upload.png')
17 | if (!fs.existsSync(FIXTURES_DIR)) {
18 | fs.mkdirSync(FIXTURES_DIR, { recursive: true })
19 | }
20 | if (!fs.existsSync(TEST_IMAGE_PATH)) {
21 | const pngData = Buffer.from(
22 | 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
23 | 'base64'
24 | )
25 | fs.writeFileSync(TEST_IMAGE_PATH, pngData)
26 | }
27 |
28 | test.describe('OSS 直传上传', () => {
29 |
30 | // 单独给这个测试更长的超时
31 | test.setTimeout(60000)
32 |
33 | test('登录 -> 赛事创建页 -> 上传封面图片到 OSS', async ({ page }) => {
34 | // 监听网络请求,捕获 OSS 相关请求
35 | const ossTokenRequests: string[] = []
36 | const ossUploadRequests: string[] = []
37 |
38 | page.on('request', (req) => {
39 | const url = req.url()
40 | if (url.includes('/upload/oss/token')) {
41 | ossTokenRequests.push(url)
42 | }
43 | if (url.includes('aliyuncs.com')) {
44 | ossUploadRequests.push(url)
45 | }
46 | })
47 |
48 | // ========== 1. 登录 ==========
49 | await page.goto(`/${TENANT_CODE}/login`)
50 | await page.waitForLoadState('domcontentloaded')
51 |
52 | // 填写 Ant Design 表单
53 | await page.locator('input[placeholder="请输入用户名"]').fill(USERNAME)
54 | await page.locator('input[placeholder="请输入密码"]').fill(PASSWORD)
55 |
56 | // 点击登录按钮(Ant Design a-button html-type="submit")
57 | await page.locator('button.login-btn, button:has-text("登录"):visible').first().click()
58 |
59 | // 等待登录成功跳转
60 | await page.waitForURL(`**/${TENANT_CODE}/**`, { timeout: 15000 })
61 | await page.waitForLoadState('domcontentloaded')
62 |
63 | // 确认不在登录页了
64 | const currentUrl = page.url()
> 65 | expect(currentUrl).not.toContain('/login')
| ^ Error: expect(received).not.toContain(expected) // indexOf
66 | console.log('[1] 登录成功, 当前页面:', currentUrl)
67 |
68 | // ========== 2. 进入赛事创建页 ==========
69 | await page.goto(`/${TENANT_CODE}/contests/create`)
70 | await page.waitForLoadState('domcontentloaded')
71 |
72 | // 等待表单页面加载
73 | await page.locator('input[placeholder*="活动名称"], input[placeholder*="名称"]').first().waitFor({ timeout: 10000 })
74 | console.log('[2] 赛事创建页加载成功')
75 |
76 | // ========== 3. 上传封面图片 ==========
77 | // 直接用全局的 file input(Ant Design Upload 的隐藏 input)
78 | const fileInputs = page.locator('input[type="file"]')
79 | const fileCount = await fileInputs.count()
80 | console.log('[3] 发现 file input 数量:', fileCount)
81 |
82 | // 第一个 file input 对应封面上传
83 | await fileInputs.first().setInputFiles(TEST_IMAGE_PATH)
84 |
85 | console.log('[3] 已选择封面文件,等待 OSS 上传...')
86 |
87 | // 等待网络请求完成
88 | await page.waitForTimeout(5000)
89 |
90 | // ========== 4. 验证 ==========
91 | console.log('[4] OSS Token 请求数:', ossTokenRequests.length)
92 | console.log('[4] OSS 上传请求数:', ossUploadRequests.length)
93 |
94 | // 验证:发出了 OSS Token 请求
95 | if (ossTokenRequests.length > 0) {
96 | console.log('[4] Token 请求 URL:', ossTokenRequests[0])
97 | }
98 |
99 | // 验证:发出了 OSS 上传请求
100 | if (ossUploadRequests.length > 0) {
101 | console.log('[4] 上传目标 URL:', ossUploadRequests[0])
102 | expect(ossUploadRequests[0]).toContain('aliyuncs.com')
103 | }
104 |
105 | // 验证:检查页面上是否有上传成功的 UI 指示
106 | const successItems = page.locator('.ant-upload-list-item-done')
107 | const errorItems = page.locator('.ant-upload-list-item-error')
108 | const successCount = await successItems.count()
109 | const errorCount = await errorItems.count()
110 |
111 | console.log('[4] 上传成功项:', successCount, '上传失败项:', errorCount)
112 |
113 | // 检查是否有错误提示消息
114 | const errorMsg = await page.locator('.ant-message-error').textContent().catch(() => '')
115 | if (errorMsg) {
116 | console.log('[4] 错误消息:', errorMsg)
117 | }
118 |
119 | // 核心断言
120 | expect(ossTokenRequests.length).toBeGreaterThanOrEqual(1)
121 | expect(ossUploadRequests.length).toBeGreaterThanOrEqual(1)
122 | expect(errorCount).toBe(0)
123 |
124 | console.log('\n===== OSS 直传上传测试通过 =====')
125 | console.log('OSS Token 请求:', ossTokenRequests.length)
126 | console.log('OSS 上传请求:', ossUploadRequests.length)
127 | console.log('上传目标:', ossUploadRequests[0] || 'N/A')
128 | })
129 | })
130 |