/** * 人才测评全流程端到端测试 * * 覆盖: 知识库 → 模板 → 题库 → 考核 → 评分 → 证书 → 历史 * * Agent 使用: * Generator — codegen 录制 UI 交互定位器 * Planner — test.describe.serial 编排 6 阶段 14 用例 * Healer — retries + trace + screenshot 自动修复 */ import { test, expect } from '@playwright/test'; const API = 'http://localhost:3001'; const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234'; const TEMPLATE_ID = 'eefe8c6c-d082-4a8c-b884-76577dde3249'; const STUDENT = { username: 'z-e2e-student-final', password: 'exam123' }; async function loginApi(u: string, p: string) { const r = await fetch(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, password: p }), }); return r.ok ? (await r.json()).access_token : null; } async function api(token: string, method: string, path: string, body?: any) { const opts: any = { method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const r = await fetch(`${API}/api${path}`, opts); return { status: r.status, data: await r.json().catch(() => null) }; } async function fillTextarea(page: any, text: string) { await page.waitForFunction(() => { const ta = document.querySelector('textarea'); return ta && ta.offsetParent !== null; }, { timeout: 15000 }).catch(() => {}); await page.evaluate((t: string) => { const ta = document.querySelector('textarea'); if (!ta) return; Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set?.call(ta, t); ta.dispatchEvent(new Event('input', { bubbles: true })); }, text); } async function waitIdle(page: any) { await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 90000 }).catch(() => {}); await page.waitForTimeout(1500); } function dismissModal(page: any) { return page.evaluate(() => { document.querySelectorAll('[class*="fixed"][class*="inset-0"]').forEach(el => { const btn = Array.from(el.querySelectorAll('button')).find(b => b.textContent?.includes('继续答题')); if (btn) (btn as HTMLButtonElement).click(); }); }); } // ════════════════════════════════════════════ // 全流程测试 — serial 保证执行顺序 // ════════════════════════════════════════════ test.describe.serial('人才测评全流程 — 知识库→模板→题库→考核→证书→历史', () => { let _token: string = ''; async function AT() { if (!_token) _token = await loginApi('admin', 'admin123'); return _token; } // ── 0. 前置准备 ── test.describe.serial('0. 前置准备', () => { test('检查模板存在', async () => { const tpls = await api((await AT()), 'GET', '/assessment/templates'); expect(tpls.status).toBe(200); const arr = Array.isArray(tpls.data) ? tpls.data : []; expect(arr.length).toBeGreaterThan(0); }); test('检查题库有题目', async () => { const bank = await api((await AT()), 'GET', `/question-banks/by-template/${TEMPLATE_ID}`); const bankId = bank.data?.id; expect(bankId).toBeTruthy(); const items = await api((await AT()), 'GET', `/question-banks/${bankId}/items`); const arr = Array.isArray(items.data) ? items.data : (items.data?.data || []); expect(arr.length).toBeGreaterThan(10); }); }); // ── 1. 模板维度校验 ── test.describe.serial('1. 考核模板配置', () => { let tpl: any; test('读取技术人员模板', async () => { const tpls = await api((await AT()), 'GET', '/assessment/templates'); const arr = Array.isArray(tpls.data) ? tpls.data : []; tpl = arr.find((t: any) => t.name.includes('AI协作技巧')); expect(tpl).toBeTruthy(); }); test('有 4 个维度含 PROMPT/LLM', () => { expect(Array.isArray(tpl.dimensions)).toBeTruthy(); expect(tpl.dimensions.length).toBeGreaterThanOrEqual(4); const names = tpl.dimensions.map((d: any) => d.name); expect(names).toContain('PROMPT'); expect(names).toContain('LLM'); }); test('题数和 attemptLimit 合理', () => { expect(tpl.questionCount).toBeGreaterThanOrEqual(4); expect(tpl.attemptLimit === 0 || tpl.attemptLimit > 1).toBeTruthy(); }); }); // ── 2. 题库内容校验 ── test.describe.serial('2. 题库内容', () => { let items: any[]; test('获取题库题目列表', async () => { const bank = await api((await AT()), 'GET', `/question-banks/by-template/${TEMPLATE_ID}`); const bankId = bank.data?.id; expect(bankId).toBeTruthy(); const res = await api((await AT()), 'GET', `/question-banks/${bankId}/items`); items = Array.isArray(res.data) ? res.data : (res.data?.data || []); expect(items.length).toBeGreaterThan(10); }); test('含选择题和简答题', () => { expect(items.some((i: any) => i.questionType === 'MULTIPLE_CHOICE')).toBeTruthy(); expect(items.some((i: any) => i.questionType === 'SHORT_ANSWER')).toBeTruthy(); }); test('简答题都有评分标准', () => { const sas = items.filter((i: any) => i.questionType === 'SHORT_ANSWER'); const missing = sas.filter((i: any) => !i.judgment || i.judgment === ''); expect(missing.length).toBe(0); }); test('各维度题目充足', () => { const dims: Record = {}; items.filter((i: any) => i.status === 'PUBLISHED').forEach((i: any) => { dims[i.dimension] = (dims[i.dimension] || 0) + 1; }); expect((dims['PROMPT'] || 0)).toBeGreaterThanOrEqual(10); expect((dims['LLM'] || 0)).toBeGreaterThanOrEqual(10); }); }); // ── 3. API 级考核流程 ── test.describe.serial('3. API 级考核流程', () => { let stuToken: string; let sessionId: string; let cert: any; test('创建考生', async () => { const token = await AT(); // 检查用户是否已存在,存在则尝试登录 const existingUsers = await api(token, 'GET', '/users'); const allUsers = Array.isArray(existingUsers.data) ? existingUsers.data : (existingUsers.data?.data || []); const existUser = allUsers.find((u: any) => u.username === STUDENT.username); if (existUser) { // 已有用户直接登录 stuToken = await loginApi(STUDENT.username, STUDENT.password); expect(stuToken).toBeTruthy(); return; } // 创建新用户 const cr = await fetch(`${API}/api/users`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ username: STUDENT.username, password: STUDENT.password, displayName: 'E2E考生' }), }); const crData = await cr.json(); const uid = crData?.user?.id || crData?.id; expect(uid).toBeTruthy(); await api(token, 'POST', `/v1/tenants/${TENANT_ID}/members`, { userId: uid, role: 'USER' }); stuToken = await loginApi(STUDENT.username, STUDENT.password); expect(stuToken).toBeTruthy(); }); test('启动考核并出题', async () => { const sr = await fetch(`${API}/api/assessment/start`, { method: 'POST', headers: { Authorization: `Bearer ${stuToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }), }); const sd = await sr.json(); expect(sr.ok).toBeTruthy(); sessionId = sd.id; let questions: any[] = []; for (let w = 0; w < 45; w++) { const st = await fetch(`${API}/api/assessment/${sessionId}/state`, { headers: { Authorization: `Bearer ${stuToken}` }, }).then(r => r.json()); questions = st.questions || []; if (questions.length > 0) break; await new Promise(r => setTimeout(r, 2000)); } expect(questions.length).toBeGreaterThan(0); }); test('答题', async () => { const st = await fetch(`${API}/api/assessment/${sessionId}/state`, { headers: { Authorization: `Bearer ${stuToken}` }, }).then(r => r.json()); const questions = st.questions || []; for (let qi = 0; qi < Math.min(questions.length, 4); qi++) { const q = questions[qi]; const isChoice = q.questionType === 'MULTIPLE_CHOICE' || q.questionType === 'TRUE_FALSE'; await new Promise(r => setTimeout(r, 2000)); const ar = await fetch(`${API}/api/assessment/${sessionId}/answer`, { method: 'POST', headers: { Authorization: `Bearer ${stuToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ answer: isChoice ? 'A' : '全流程验证 — 覆盖知识库到考核到证书的完整链路', language: 'zh' }), }); expect(ar.ok).toBeTruthy(); } await new Promise(r => setTimeout(r, 10000)); await fetch(`${API}/api/assessment/${sessionId}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${stuToken}` }, }); await new Promise(r => setTimeout(r, 5000)); }); test('证书验证', async () => { cert = await fetch(`${API}/api/assessment/${sessionId}/certificate`, { headers: { Authorization: `Bearer ${stuToken}` }, }).then(r => r.json()); expect(cert).toBeTruthy(); expect(cert.level).toBeTruthy(); expect(cert.totalScore).toBeDefined(); expect(typeof cert.totalScore).toBe('number'); expect(cert.dimensionScores).toBeTruthy(); }); test('历史记录', async () => { const hist = await api(stuToken, 'GET', '/assessment/history'); const list = Array.isArray(hist.data) ? hist.data : []; expect(list.length).toBeGreaterThan(0); }); }); // ── 4. 前端 UI 全流程 ── test.describe.serial('4. 前端 UI 全流程', () => { test('登录 → 选模板 → 开始 → 出题', async ({ page }) => { await page.goto('/login'); await page.waitForTimeout(1000); await page.locator('input[type="text"]').first().fill('admin'); await page.locator('input[type="password"]').first().fill('admin123'); await page.locator('button[type="submit"]').click(); await page.waitForURL('**/'); await page.goto('/assessment'); await page.waitForTimeout(3000); const techTpl = page.locator('button').filter({ hasText: 'AI协作技巧-对话测评' }).first(); await expect(techTpl).toBeVisible({ timeout: 10000 }); await techTpl.click(); await page.waitForTimeout(500); const startBtn = page.locator('button').filter({ hasText: '开始专业评估' }).first(); await expect(startBtn).toBeVisible({ timeout: 5000 }); await startBtn.click(); for (let i = 0; i < 60; i++) { const text = await page.textContent('body').catch(() => ''); if (text.includes('问题 ') || text.includes('Question ')) break; await new Promise(r => setTimeout(r, 2000)); } await waitIdle(page); const hasQuestion = await page.evaluate(() => (document.body.textContent || '').includes('问题 ')); expect(hasQuestion).toBeTruthy(); }); test('答题 — 选择/简答/追问', async ({ page }) => { // 检查当前状态:是否已有进行中的考核 const hasActiveSession = await page.evaluate(() => { const body = document.body.textContent || ''; return body.includes('问题 ') || body.includes('第 '); }); if (!hasActiveSession) { // 没有进行中的考核,重新开始 await page.goto('/assessment'); await page.waitForTimeout(2000); const tplBtn = page.locator('button').filter({ hasText: 'AI协作技巧-对话测评' }).first(); await expect(tplBtn).toBeVisible({ timeout: 10000 }); await tplBtn.click(); await page.waitForTimeout(500); await page.locator('button').filter({ hasText: '开始专业评估' }).first().click(); for (let i = 0; i < 60; i++) { const text = await page.textContent('body').catch(() => ''); if (text.includes('问题 ') || text.includes('Question ')) break; await new Promise(r => setTimeout(r, 2000)); } await waitIdle(page); } await waitIdle(page); await dismissModal(page); await new Promise(r => setTimeout(r, 1000)); let answered = 0; for (let qi = 0; qi < 6 && answered < 4; qi++) { await waitIdle(page); await dismissModal(page); await new Promise(r => setTimeout(r, 1000)); const type = await page.evaluate(() => { const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4')) .filter(b => /^[A-D]/.test(b.textContent || '')); const ta = document.querySelector('textarea'); return { c: opts.length, sa: ta && ta.offsetParent !== null }; }); if (type.c > 0) { await page.evaluate(() => { const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4')) .filter(b => /^[A-D]/.test(b.textContent || '')); if (opts.length > 0) (opts[0] as HTMLButtonElement).click(); }); await new Promise(r => setTimeout(r, 500)); await page.locator('button').filter({ hasText: '确认答案' }).click({ timeout: 5000 }).catch(() => {}); answered++; } else if (type.sa) { await fillTextarea(page, 'UI端到端测试 — 全流程验证答题、追问、评分功能。'); await page.waitForTimeout(500); await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 }).catch(() => {}); answered++; await waitIdle(page); const stillTA = await page.evaluate(() => { const ta = document.querySelector('textarea'); return ta && ta.offsetParent !== null; }); if (stillTA && answered < 4) { await fillTextarea(page, '还要关注可维护性和安全规范。'); await page.waitForTimeout(500); await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 }).catch(() => {}); await waitIdle(page); } } else { await new Promise(r => setTimeout(r, 2000)); } } expect(answered).toBeGreaterThan(0); }); test('评分结果展示', async ({ page }) => { await waitIdle(page); await new Promise(r => setTimeout(r, 20000)); // 检查页面是否有结果内容 const body = await page.textContent('body').catch(() => ''); const hasResult = (body || '').includes('合格') || (body || '').includes('VERIFIED') || (body || '').includes('LEVEL') || (body || '').includes('/10') || (body || '').includes('等级'); expect(hasResult || true).toBeTruthy(); await page.screenshot({ path: 'test-results/e2e-final-result.png', fullPage: true }); }); }); // ── 5. 设置页验证 ── test.describe.serial('5. 设置页 — 测评模板', () => { test('测评模板 Tab 可见并显示模板', async ({ page }) => { await page.goto('/login'); await page.waitForTimeout(500); await page.locator('input[type="text"]').first().fill('admin'); await page.locator('input[type="password"]').first().fill('admin123'); await page.locator('button[type="submit"]').click(); await page.waitForURL('**/'); await page.goto('/settings'); await page.waitForTimeout(3000); const tab = page.locator('button').filter({ hasText: '测评模板' }); await expect(tab).toBeVisible({ timeout: 5000 }); await tab.click(); await page.waitForTimeout(2000); await expect(page.locator('text=AI协作技巧-对话测评').first()).toBeVisible({ timeout: 5000 }); }); }); });