后端新增 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>
317 lines
12 KiB
Markdown
317 lines
12 KiB
Markdown
# 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
|
||
|
||
```yaml
|
||
- 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
|
||
|
||
```ts
|
||
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 |
|
||
``` |