# 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 | ```