Files
L2keka/web/e2e/ui-completeness.spec.ts
hangshuo652 6a2419d4a4 L2考核落地:人才测评整合为L2考核track,新增查重初筛与e2e验证
- 新增 L2考核 track(7维标准/100分):选题难度赋分cap、功能完整性地板线、合格判定
- 人才测评整合进 L2考核:移除 L2/L3 两级认定与 question_id 机制
- 新增 l2-topics 选题元数据服务与 config/l2-topics.json(11命题题+自选题)
- 新增查重初筛 plagiarism-detect(MD5精确比对+归一化相似度+提交行为,仅告警)
- 修复 L2 维度 DIM_FILE_FILTERS 缺失导致 AI 协作记录证据漏喂
- e2e:修复 Windows spawn、DB 隔离,L2 用例 27 项全绿
2026-08-23 20:58:56 +08:00

237 lines
11 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 } from '@playwright/test';
const PASSWORD = 'test123';
const UNIQUE = Date.now().toString(36);
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('/');
}
async function apiToken(request: any): Promise<string> {
const res = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
const { token } = await res.json();
return token;
}
// ══════════════════════════════════════
// 1. 成果物 TabDeliverablesView
// ══════════════════════════════════════
test.describe('成果物 Tab', () => {
let pid = '';
let token = '';
test.beforeAll(async ({ request }) => {
token = await apiToken(request);
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` }, data: { name: `deliv-${UNIQUE}`, track: '赛道一' },
});
pid = (await proj.json()).id;
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
headers: { Authorization: `Bearer ${token}` }, data: { name: 'd-std', content: '## 场景价值(8分)' },
});
await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { title: 'deliv-entry', repo_url: `file://C:\\deliv-${UNIQUE}` },
});
});
test.afterAll(async ({ request }) => {
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
});
test('初始化一覧 → 勾选 → 提交率更新 → CSV 下载', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.getByRole('button', { name: '成果物' }).click();
await expect(page.getByText('成果物确认')).toBeVisible();
// 初始化一覧
await page.getByRole('button', { name: '初始化一覧' }).click();
await expect(page.locator('.entry-table input[type="checkbox"]').first()).toBeVisible();
// 勾选第一个成果物 → 提交率 +1(经 API 验证)
const before = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/deliverables/summary`, {
headers: { Authorization: `Bearer ${token}` },
});
const beforeData = await before.json();
await page.locator('.entry-table input[type="checkbox"]').first().check();
await expect.poll(async () => {
const r = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/deliverables/summary`, {
headers: { Authorization: `Bearer ${token}` },
});
return (await r.json()).totalSubmitted;
}).toBe(beforeData.totalSubmitted + 1);
// CSV 下载
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: '下载CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('deliverables');
});
});
// ══════════════════════════════════════
// 2. 评审进度时间线(DetailPanelD7
// ══════════════════════════════════════
test.describe('评审进度时间线', () => {
let pid = ''; let eid = ''; let token = '';
test.beforeAll(async ({ request }) => {
token = await apiToken(request);
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` }, data: { name: `prog-${UNIQUE}`, track: '赛道一' },
});
pid = (await proj.json()).id;
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
headers: { Authorization: `Bearer ${token}` }, data: { name: 'p-std', content: '## 场景价值(8分)' },
});
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { title: 'prog-entry', repo_url: `file://C:\\prog-${UNIQUE}` },
});
eid = (await entry.json()).id;
// 启动评审(外部路径 → 秒级 clone_fail),写入 progress_log
await request.post(`http://localhost:3002/api/projects/${pid}/entries/${eid}/start`, {
headers: { Authorization: `Bearer ${token}` },
});
await expect.poll(async () => {
const r = await request.get(`http://localhost:3002/api/projects/${pid}/entries/${eid}`, {
headers: { Authorization: `Bearer ${token}` },
});
return (await r.json()).status;
}).toBe('clone_fail');
});
test.afterAll(async ({ request }) => {
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
});
test('详情面板显示评审进度时间线(含克隆步骤)', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.click('.title-cell');
const progress = page.locator('details', { has: page.getByText('评审进度') });
await expect(progress).toBeVisible();
await expect(progress).toContainText('不允许克隆外部路径');
});
});
// ══════════════════════════════════════
// 3. PDF 下载(单条目报告 / 汇总报告)
// ══════════════════════════════════════
test.describe('PDF 下载', () => {
let pid = ''; let eid = ''; let token = '';
test.beforeAll(async ({ request }) => {
token = await apiToken(request);
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` }, data: { name: `pdf-${UNIQUE}`, track: '赛道一' },
});
pid = (await proj.json()).id;
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
headers: { Authorization: `Bearer ${token}` }, data: { name: 'pdf-std', content: '## 架构设计(10分)' },
});
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { title: 'pdf-entry', repo_url: `file://C:\\pdf-${UNIQUE}` },
});
eid = (await entry.json()).id;
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { dimensions: [{ name: '架构设计', score: 8, maxScore: 10 }] },
});
});
test.afterAll(async ({ request }) => {
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
});
test('单条目报告 PDF 下载', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.click('.title-cell');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: '下载报告' }).click();
const download = await downloadPromise;
// 服务端 Content-Disposition 中文文件名在部分平台会被搅乱,这里只断言"触发了 .pdf 下载"
expect(download.suggestedFilename().toLowerCase()).toContain('.pdf');
});
test('汇总报告 PDF 下载', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.getByRole('button', { name: '汇总' }).click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: '下载汇总PDF' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename().toLowerCase()).toContain('.pdf');
});
});
// ══════════════════════════════════════
// 4. L2考核:条目列表合格/不合格徽章展示
// ══════════════════════════════════════
test.describe('L2考核 合格/不合格徽章', () => {
let pid = ''; let eid = ''; let token = '';
test.beforeAll(async ({ request }) => {
token = await apiToken(request);
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` }, data: { name: `l2badge-${UNIQUE}`, track: 'L2考核' },
});
pid = (await proj.json()).id;
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { title: 'l2-entry', repo_url: `file://C:\\l2-${UNIQUE}`, selected_topic: '11' },
});
eid = (await entry.json()).id;
// 播种评审结果:总分 70 ≥60 且 功能完整性 25 ≥15 → 合格
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: {
dimensions: [
{ name: '功能完整性', score: 25, maxScore: 30 },
{ name: '设计文档', score: 8, maxScore: 10 },
{ name: '测试用例与测试结果', score: 7, maxScore: 10 },
{ name: 'AI协作过程记录', score: 10, maxScore: 15 },
{ name: '技术选型与范式运用', score: 10, maxScore: 15 },
{ name: '代码质量+README', score: 6, maxScore: 10 },
{ name: '业务场景理解与需求分析', score: 4, maxScore: 10 },
],
},
});
// 触发合格判定(PUT /report 重算 final_level
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/report`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { dimensions: [
{ name: '功能完整性', score: 25, maxScore: 30 },
{ name: '设计文档', score: 8, maxScore: 10 },
{ name: '测试用例与测试结果', score: 7, maxScore: 10 },
{ name: 'AI协作过程记录', score: 10, maxScore: 15 },
{ name: '技术选型与范式运用', score: 10, maxScore: 15 },
{ name: '代码质量+README', score: 6, maxScore: 10 },
{ name: '业务场景理解与需求分析', score: 4, maxScore: 10 },
] },
});
});
test.afterAll(async ({ request }) => {
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
});
test('列表展示「合格」徽章(L2考核 判定)', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await expect(page.getByText('合格', { exact: true })).toBeVisible();
// 条目还展示选题徽章
await expect(page.getByText('命题 11')).toBeVisible();
});
});