library-picturebook-activity/frontend/test-results/upload-oss-upload-OSS-直传上传-登录---赛事创建页---上传封面图片到-OSS-chromium/error-context.md
En b9ed5e17c6 feat: OSS 客户端直传改造(STS Token 签发 + 前端直传 + CORS 自动配置)
后端新增 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>
2026-04-08 15:19:43 +08:00

12 KiB
Raw Blame History

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 inputAnt 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 |