Files
L2keka/web/e2e/full-e2e.spec.ts

795 lines
33 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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();
});
});