import { test, expect, type Page, type APIRequestContext } from '@playwright/test'; const PASSWORD = 'test123'; const UNIQUE = Date.now().toString(36); const BASE = 'http://localhost:3002/api'; let apiCtx: APIRequestContext; // 赛道必选后项目创建自动带入赛道默认标准;若测试只用自定义标准,需先清空自动标准, // 让条目创建时 resolveStandard 回退到用户上传的标准(category_tag='') async function apiClearStandards(request: APIRequestContext, token: string, pid: string): Promise { const list = await request.fetch(`${BASE}/projects/${pid}/standards`, { headers: { Authorization: `Bearer ${token}` } }); const standards = await list.json(); for (const s of standards) { await request.fetch(`${BASE}/projects/${pid}/standards/${s.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); } } // ─── Helper: login via UI, then create API context ─── async function login(page: Page) { await page.goto('/login'); await page.fill('input[type="password"]', PASSWORD); await page.click('button[type="submit"]'); await page.waitForURL('/'); } // ─── Helper: api call via page.request ─── async function api(page: Page, method: string, path: string, body?: any) { const res = await page.request.fetch(`http://localhost:3002${path}`, { method, headers: { 'Content-Type': 'application/json' }, data: body, }); return { status: res.status(), data: await res.json() }; } // ══════════════════════════════════════ // 1. 认证测试 // ══════════════════════════════════════ test.describe('认证', () => { test('1.1 未登录跳转到登录页', async ({ page }) => { await page.goto('/'); await page.waitForURL('/login'); expect(page.url()).toContain('/login'); }); test('1.2 空密码按钮禁用', async ({ page }) => { await page.goto('/login'); await expect(page.locator('button[type="submit"]')).toBeDisabled(); }); test('1.3 错误密码显示错误', async ({ page }) => { await page.goto('/login'); await page.fill('input[type="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); await expect(page.locator('.error')).toContainText('密码错误'); }); test('1.4 正确密码登录成功', async ({ page }) => { await login(page); await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review'); }); test('1.5 token 持久化(刷新后仍登录)', async ({ page }) => { await login(page); await page.reload(); await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review'); }); test('1.6 退出登录', async ({ page }) => { await login(page); await page.click('.sidebar-header button:has-text("退出")'); await page.waitForURL('/login'); }); }); // ══════════════════════════════════════ // 2. 侧边栏 & 项目 // ══════════════════════════════════════ test.describe('侧边栏 & 项目', () => { test.beforeEach(async ({ page }) => { await login(page); }); test('2.1 空项目列表显示"新建项目"按钮', async ({ page }) => { await expect(page.locator('.btn-new-project')).toHaveText('+ 新建项目'); }); test('2.2 取消创建项目', async ({ page }) => { await page.click('.btn-new-project'); await expect(page.locator('.new-project-form input')).toBeVisible(); await page.click('.new-project-actions button:has-text("取消")'); await expect(page.locator('.btn-new-project')).toBeVisible(); }); test('2.3 创建项目', async ({ page }) => { await page.click('.btn-new-project'); await page.fill('.new-project-form input', `测试项目-${UNIQUE}`); await page.selectOption('.new-project-form select', '赛道一'); await page.click('.new-project-actions button:first-child'); await page.waitForURL(/\/project\//); await expect(page.locator('.project-view h2')).toContainText(`测试项目-${UNIQUE}`); }); test('2.4 项目统计显示', async ({ page }) => { await page.goto('/'); const first = page.locator('.project-stats').first(); await expect(first).toBeVisible(); }); test('2.5 项目激活高亮', async ({ page }) => { await page.click('.project-item:first-child'); await expect(page.locator('.project-item.active')).toBeVisible(); }); test('2.6 仪表盘显示', async ({ page }) => { await page.goto('/'); await expect(page.locator('.dashboard h1')).toContainText('仪表盘'); }); }); // ══════════════════════════════════════ // 3. 评审标准 // ══════════════════════════════════════ test.describe('评审标准', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { name: `标准测试-${UNIQUE}`, track: '赛道一' }, }); expect(pr.status()).toBe(200); pid = (await pr.json()).id; await apiClearStandards(request, token, pid); }); test.beforeEach(async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("标准")'); }); test('3.1 空标准显示提示', async ({ page }) => { await expect(page.locator('.empty')).toContainText('暂无评审标准'); }); test('3.2 上传标准 - 显示表单', async ({ page }) => { await page.click('.section-header button:has-text("上传标准")'); await expect(page.locator('.standard-form')).toBeVisible(); }); test('3.3 上传标准 - 无效格式', async ({ page }) => { await page.click('.section-header button:has-text("上传标准")'); await page.fill('.standard-form input:first-child', '无效标准'); await page.fill('.standard-form textarea', '没有格式的普通文本'); await page.click('.form-actions button:has-text("保存")'); await expect(page.locator('.standard-form')).toBeVisible(); }); test('3.4 上传标准 - 总分超过上限(150)被拒', async ({ page }) => { await page.click('.section-header button:has-text("上传标准")'); await page.fill('.standard-form input:first-child', '超分标准'); await page.fill('.standard-form textarea', '## 维度一(100分)\n内容\n## 维度二(60分)\n内容'); await page.click('.form-actions button:has-text("保存")'); // 被拒绝 → 表单保持打开 await expect(page.locator('.standard-form')).toBeVisible(); }); test('3.5 上传标准 - 正常创建(含分类标签)', async ({ page }) => { await page.click('.section-header button:has-text("上传标准")'); await page.fill('.standard-form input:first-child', '算法标准'); await page.fill('.standard-form input:nth-child(2)', '算法'); await page.fill('.standard-form textarea', '## 代码质量(30分)\n代码整洁度\n## 架构设计(40分)\n模块化\n## 测试覆盖(30分)\n覆盖率'); await page.click('.form-actions button:has-text("保存")'); await expect(page.locator('.standard-card')).toHaveCount(1); await expect(page.locator('.standard-card')).toContainText('算法标准'); await expect(page.locator('.standard-card')).toContainText('算法'); await expect(page.locator('.standard-card')).toContainText('代码质量(30分)'); await expect(page.locator('.standard-card')).toContainText('架构设计(40分)'); }); test('3.6 标准数量显示', async ({ page }) => { await expect(page.locator('.section-header h3')).toContainText('评审标准 (1)'); }); test('3.7 删除标准', async ({ page }) => { page.on('dialog', d => d.accept()); await page.click('.btn-danger'); await expect(page.locator('.empty')).toContainText('暂无评审标准'); }); }); // ══════════════════════════════════════ // 4. 条目管理 // ══════════════════════════════════════ test.describe('条目管理', () => { let pid = ''; const entryTitle = `选手A-${UNIQUE}`; const repoUrl = `https://example.com/${UNIQUE}.git`; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: auth, data: { name: `条目测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/standards`, { method: 'POST', headers: auth, data: { name: '默认标准', content: '## 代码质量(50分)\n整洁\n## 架构设计(50分)\n设计' }, }); }); test.beforeEach(async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("条目")'); }); test('4.1 空条目显示提示', async ({ page }) => { await expect(page.locator('.empty-row')).toContainText('暂无条目'); }); test('4.2 筛选器存在', async ({ page }) => { await expect(page.locator('.filter-select')).toBeVisible(); await expect(page.locator('.search-input')).toBeVisible(); }); test('4.3 全部筛选选项', async ({ page }) => { const opts = await page.locator('.filter-select option').allTextContents(); expect(opts).toContain('全部'); expect(opts).toContain('待评审'); expect(opts).toContain('排队中'); expect(opts).toContain('克隆中'); expect(opts).toContain('分析中'); expect(opts).toContain('已完成'); expect(opts).toContain('已修正'); expect(opts).toContain('克隆失败'); expect(opts).toContain('分析失败'); expect(opts).toContain('失败'); }); test('4.4 通过API创建条目 → 页面可见', async ({ page, request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { title: entryTitle, repo_url: repoUrl, participant: '张三', difficulty: '★★★' }, }); await page.reload(); await expect(page.locator('.entry-table')).toContainText(entryTitle); }); test('4.5 条目列表显示字段', async ({ page }) => { await expect(page.locator('.entry-table')).toContainText('张三'); await expect(page.locator('.entry-table')).toContainText('待评审'); await expect(page.locator('.entry-table')).toContainText('-'); }); test('4.6 "启动"按钮存在', async ({ page }) => { await expect(page.locator('.btn-action:has-text("启动")')).toBeVisible(); }); test('4.7 启动条目', async ({ page }) => { await page.click('.entry-table .btn-action:has-text("启动")'); await expect(page.locator('.entry-table .badge').first()).toBeVisible(); }); test('4.8 取消评审', async ({ page, request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const er = await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: auth, data: { title: `取消测试-${UNIQUE}`, repo_url: `https://cancel-${UNIQUE}.git` }, }); const eid = (await er.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/entries/${eid}/start`, { method: 'POST', headers: auth, }); await page.reload(); await expect(page.locator('.entry-table')).toContainText('取消测试'); await expect(page.locator('.entry-table .badge').first()).toBeVisible(); }); test('4.9 搜索功能', async ({ page }) => { await page.fill('.search-input', '选手A'); await page.waitForTimeout(500); await page.press('.search-input', 'Enter'); await expect(page.locator('.entry-table')).toContainText('选手A', { timeout: 10000 }); }); test('4.10 状态筛选', async ({ page }) => { await page.selectOption('.filter-select', 'pending'); await page.waitForTimeout(500); const badges = await page.locator('.entry-table .badge').allTextContents(); const statusTexts = badges.filter(b => !['赛道一', '新規', '修正'].includes(b.trim())); for (const b of statusTexts) { expect(b.trim()).toBe('待评审'); } }); }); // ══════════════════════════════════════ // 5. 批量导入 // ══════════════════════════════════════ test.describe('批量导入', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: auth, data: { name: `导入测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/standards`, { method: 'POST', headers: auth, data: { name: '默认标准', content: '## 代码质量(50分)\n整洁\n## 架构设计(50分)\n设计' }, }); }); test.beforeEach(async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("条目")'); }); test('5.1 打开导入面板', async ({ page }) => { await page.click('.btn-secondary:has-text("导入")'); await expect(page.locator('.import-panel')).toBeVisible(); }); test('5.2 批量导入 CSV 格式', async ({ page }) => { await page.click('.btn-secondary:has-text("导入")'); const csv = `title,repo_url,participant,difficulty 小明作品,https://xiaoming-${UNIQUE}.git,小明,★★ 小红作品,https://xiaohong-${UNIQUE}.git,小红,★★★`; await page.fill('.import-panel textarea', csv); await page.click('.import-panel .form-actions button:first-child'); await expect(page.locator('.import-result')).toContainText('成功 2 条'); }); test('5.3 导入结果显示错误行', async ({ page }) => { await page.click('.btn-secondary:has-text("导入")'); const csv = `title,repo_url ,https://empty-title.git 有效,https://valid-${UNIQUE}.git`; await page.fill('.import-panel textarea', csv); await page.click('.import-panel .form-actions button:first-child'); await expect(page.locator('.import-result')).toContainText('成功 1 条'); await expect(page.locator('.import-result')).toContainText('失败 1 条'); await expect(page.locator('.error-row')).toContainText('标题为空'); }); test('5.4 表格显示导入的条目', async ({ page }) => { await page.waitForTimeout(500); await expect(page.locator('.section-header h3')).not.toContainText('(0)'); }); }); // ══════════════════════════════════════ // 6. 详情面板 // ══════════════════════════════════════ test.describe('详情面板', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: auth, data: { name: `详情测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/standards`, { method: 'POST', headers: auth, data: { name: '默认标准', content: '## 代码质量(50分)\n整洁\n## 架构设计(50分)\n设计' }, }); // Create an entry and mark it as review_done via report const er = await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: auth, data: { title: `详情条目-${UNIQUE}`, repo_url: `https://detail-${UNIQUE}.git` }, }); const eid = (await er.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, { method: 'PUT', headers: auth, data: { dimensions: [ { name: '代码质量', score: 40, maxScore: 50, comment: '代码整洁' }, { name: '架构设计', score: 35, maxScore: 50, comment: '合理' }, ]}, }); }); test.beforeEach(async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("条目")'); }); test('6.1 点击标题打开详情', async ({ page }) => { await page.click('.title-cell'); await expect(page.locator('.detail-panel')).toBeVisible(); await expect(page.locator('.detail-panel h3')).toContainText(`详情条目-${UNIQUE}`); }); test('6.2 详情面板显示元信息', async ({ page }) => { await page.click('.title-cell'); await expect(page.locator('.detail-meta')).toContainText(`https://detail-${UNIQUE}.git`); }); test('6.3 详情面板显示维度', async ({ page }) => { await page.click('.title-cell'); await expect(page.locator('.detail-dims')).toBeVisible(); const rows = await page.locator('.detail-dims tbody tr').count(); expect(rows).toBe(2); }); test('6.4 评分可编辑', async ({ page }) => { await page.click('.title-cell'); const input = page.locator('.score-input').first(); await input.fill('45'); await expect(input).toHaveValue('45'); }); test('6.5 评语可编辑', async ({ page }) => { await page.click('.title-cell'); // 评语为点击后进入编辑态(textarea.comment-input) await page.locator('.suggestion-cell').first().click(); const comment = page.locator('.comment-input').first(); await comment.fill('修改后评语'); await expect(comment).toHaveValue('修改后评语'); }); test('6.6 保存修正按钮存在', async ({ page }) => { await page.click('.title-cell'); await expect(page.locator('.btn-primary:has-text("保存修正")')).toBeVisible(); }); test('6.7 关闭详情面板', async ({ page }) => { await page.click('.title-cell'); await expect(page.locator('.detail-panel')).toBeVisible(); await page.click('.btn-close'); await expect(page.locator('.detail-panel')).not.toBeVisible(); }); test('6.8 点击遮罩关闭详情', async ({ page }) => { await page.click('.title-cell'); await page.waitForTimeout(300); await page.click('.detail-overlay', { position: { x: 10, y: 10 } }); await expect(page.locator('.detail-panel')).not.toBeVisible(); }); }); // ══════════════════════════════════════ // 7. 汇总视图 // ══════════════════════════════════════ test.describe('汇总视图', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: auth, data: { name: `汇总测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/standards`, { method: 'POST', headers: auth, data: { name: '标准1', category_tag: '赛道一', content: '## 代码质量(50分)\n整洁\n## 架构设计(50分)\n设计' }, }); for (let i = 0; i < 3; i++) { const er = await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: auth, data: { title: `选手${i}-${UNIQUE}`, repo_url: `https://p${i}-${UNIQUE}.git`, participant: `参赛者${i}`, category_tag: '赛道一', difficulty: i === 0 ? '★★' : '★★★' }, }); const eid = (await er.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, { method: 'PUT', headers: auth, data: { dimensions: [ { name: '代码质量', score: 40 + i * 5, maxScore: 50, comment: 'ok' }, { name: '架构设计', score: 30 + i * 5, maxScore: 50, comment: 'ok' }, ]}, }); } }); test.beforeEach(async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("汇总")'); }); test('7.1 汇总页面显示标题', async ({ page }) => { await expect(page.locator('h3')).toContainText('汇总排名'); }); test('7.2 按分类显示排名', async ({ page }) => { await expect(page.locator('.summary-section')).toBeVisible(); await expect(page.locator('.summary-section h4')).toContainText('赛道一'); }); test('7.3 排名表格中显示选手', async ({ page }) => { await expect(page.locator('.summary-section')).toContainText('参赛者0'); await expect(page.locator('.summary-section')).toContainText('参赛者1'); await expect(page.locator('.summary-section')).toContainText('参赛者2'); }); test('7.4 排名按分数降序', async ({ page }) => { const scores = await page.locator('.summary-section .entry-table td:nth-child(4)').allTextContents(); const nums = scores.map(s => parseInt(s)); for (let i = 1; i < nums.length; i++) { expect(nums[i]).toBeLessThanOrEqual(nums[i - 1]); } }); test('7.5 排名序号 #1 #2 #3', async ({ page }) => { const ranks = await page.locator('.rank').allTextContents(); expect(ranks).toEqual(['#1', '#2', '#3']); }); test('7.6 参赛者合格判定区域', async ({ page }) => { await expect(page.locator('h4:has-text("参赛者合格判定")')).toBeVisible(); }); test('7.7 无参赛者时显示空状态', async ({ page, request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { name: `空汇总-${UNIQUE}`, track: '赛道一' }, }); const emptyPid = (await pr.json()).id; await login(page); await page.goto(`/project/${emptyPid}`); await page.click('.tab:has-text("汇总")'); await expect(page.locator('h3')).toContainText('汇总排名'); await expect(page.locator('.summary-section')).toHaveCount(0); }); }); // ══════════════════════════════════════ // 8. 异常与临界值测试 // ══════════════════════════════════════ test.describe('异常与临界值', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: auth, data: { name: `异常测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/standards`, { method: 'POST', headers: auth, data: { name: '标准', content: '## 代码质量(100分)\n质量' }, }); }); test('8.1 导航到不存在的项目', async ({ page }) => { await login(page); // navigating to non-existent project should show error or fallback await page.goto('/project/non-existent-id'); // Should show something that indicates error or at least not crash const body = await page.locator('.project-view').textContent(); expect(body).toBeTruthy(); }); test('8.2 空标题创建条目被阻止(通过API验证)', async ({ page, request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const er = await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { repo_url: 'https://no-title.git' }, }); expect(er.status()).toBe(400); }); test('8.3 删除标准前确认弹窗', async ({ page, request }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("标准")'); // Create a standard to delete await page.click('.section-header button:has-text("上传标准")'); await page.fill('.standard-form input:first-child', '待删除标准'); await page.fill('.standard-form textarea', '## 维度(100分)\n内容'); await page.click('.form-actions button:has-text("保存")'); await page.waitForTimeout(300); let dialogSeen = false; page.on('dialog', d => { dialogSeen = true; d.accept(); }); await page.locator('.btn-danger').first().click(); await page.waitForTimeout(300); expect(dialogSeen).toBe(true); }); test('8.4 导入空CSV', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("条目")'); await page.click('.btn-secondary:has-text("导入")'); await page.fill('.import-panel textarea', ''); await page.click('.import-panel .form-actions button:first-child'); await expect(page.locator('.import-panel')).toBeVisible(); }); test('8.5 同一个条目无法两次启动', async ({ page, request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const er = await request.fetch(`http://localhost:3002/api/projects/${pid}/entries`, { method: 'POST', headers: auth, data: { title: `双启动-${UNIQUE}`, repo_url: `https://dual-${UNIQUE}.git` }, }); const eid = (await er.json()).id; await request.fetch(`http://localhost:3002/api/projects/${pid}/entries/${eid}/start`, { method: 'POST', headers: auth, }); await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("条目")'); await page.reload(); await expect(page.locator('.btn-action').first()).toBeVisible(); }); }); // ══════════════════════════════════════ // 9. UI 一致性 // ══════════════════════════════════════ test.describe('UI 一致性', () => { let pid = ''; test.beforeAll(async ({ request }) => { const r = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD }, }); const { token } = await r.json(); const pr = await request.fetch('http://localhost:3002/api/projects', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { name: `UI测试-${UNIQUE}`, track: '赛道一' }, }); pid = (await pr.json()).id; }); test('9.1 四个Tab都存在', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await expect(page.locator('.tab')).toHaveText(['标准', '条目', '成果物', '汇总']); }); test('9.2 Tab切换工作', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('.tab:has-text("标准")'); await expect(page.locator('.tab.active')).toContainText('标准'); await page.click('.tab:has-text("条目")'); await expect(page.locator('.tab.active')).toContainText('条目'); await page.click('.tab:has-text("汇总")'); await expect(page.locator('.tab.active')).toContainText('汇总'); }); test('9.3 项目统计显示所有项', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); const meta = await page.locator('.project-meta').textContent(); expect(meta).toContain('总计'); expect(meta).toContain('✓'); expect(meta).toContain('▶'); expect(meta).toContain('✕'); }); }); // ══════════════════════════════════════ // 10. 完整用户流程 // ══════════════════════════════════════ test.describe('完整用户流程', () => { test('完整流程:创建项目→标准→条目→查看汇总', async ({ page }) => { await login(page); // Step 1: Create project await page.click('.btn-new-project'); await page.fill('.new-project-form input', `全流程-${UNIQUE}`); await page.selectOption('.new-project-form select', '赛道一'); await page.click('.new-project-actions button:first-child'); await page.waitForURL(/\/project\//); // 清掉赛道自动标准,保证下方断言 `.standard-card` 唯一(上传的「全流程标准」) const pid10 = page.url().split('/project/')[1]; const loginR = await page.request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } }); const { token: tok10 } = await loginR.json(); const listS = await page.request.get(`http://localhost:3002/api/projects/${pid10}/standards`, { headers: { Authorization: `Bearer ${tok10}` } }); const stds10 = await listS.json(); for (const s of stds10) { await page.request.delete(`http://localhost:3002/api/projects/${pid10}/standards/${s.id}`, { headers: { Authorization: `Bearer ${tok10}` } }); } // Step 2: Switch to standards tab, create standard await page.click('.tab:has-text("标准")'); await page.click('.section-header button:has-text("上传标准")'); await page.fill('.standard-form input:first-child', '全流程标准'); await page.fill('.standard-form textarea', '## 功能完整性(40分)\n功能完整\n## 代码质量(30分)\n代码整洁\n## 文档(30分)\n文档齐全'); await page.click('.form-actions button:has-text("保存")'); await expect(page.locator('.standard-card')).toContainText('全流程标准'); // Step 3: Switch to entries tab, batch import await page.click('.tab:has-text("条目")'); await page.click('.btn-secondary:has-text("导入")'); const csv = `title,repo_url,participant,difficulty 选手1,https://p1-${UNIQUE}.git,张三,★★ 选手2,https://p2-${UNIQUE}.git,李四,★★★`; await page.fill('.import-panel textarea', csv); await page.click('.import-panel .form-actions button:first-child'); await expect(page.locator('.import-result')).toContainText('成功 2 条'); // Step 4: Verify entries visible await page.waitForTimeout(500); await expect(page.locator('.section-header h3')).toContainText('(2)'); // Step 5: Switch to summary await page.click('.tab:has-text("汇总")'); // Since entries haven't been reviewed, summary should show categories await page.waitForTimeout(500); const body = await page.locator('.tab-content').textContent(); expect(body).toBeTruthy(); }); });