/** * AuraK 题库多轮对话 — Phase 1 + Phase 2 测试 * * Phase 1: 核心功能 * 1. 选择题出题并正确提交 * 2. 简答题出题 + AI 追问触发 * 3. 追问回答 + 评分反馈 * 4. 完整考核闭环(生成报告/分数) * * Phase 2: 边界测试 * 5. 空回答按钮 disabled * 6. 超长回答(5000字)提交 * 7. 连续快速点击不重复提交 * 8. 考核中刷新页面 Session 恢复 * * 用法: node qa-assessment-flow.mjs */ import { chromium } from 'playwright'; const BASE = 'http://localhost:13001'; const API = 'http://localhost:3001'; let globalPassed = 0; let globalFailed = 0; function assert(label, ok) { if (ok) { globalPassed++; console.log(` ✅ ${label}`); } else { globalFailed++; console.log(` ❌ ${label}`); } } function section(title) { console.log(`\n${'─'.repeat(50)}`); console.log(` ${title}`); console.log(`${'─'.repeat(50)}`); } async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function waitForIdle(page, timeoutMs = 60000) { for (let i = 0; i < timeoutMs / 2000; i++) { const busy = await page.evaluate(() => !!document.querySelector('.animate-spin')); if (!busy) return; await sleep(2000); } } async function dismissModal(page) { const modalBtn = page.locator('.fixed.inset-0 button, .fixed.inset-0 [class*="lucide-x"]'); if (await modalBtn.first().isVisible().catch(() => false)) { await modalBtn.first().click().catch(() => {}); await sleep(500); } } async function loginAndStartAssessment(page) { await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); await sleep(1500); 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(`${BASE}/assessment`, { waitUntil: 'networkidle' }); await sleep(2000); await page.locator('button:has-text("AI协作技巧")').first().click(); await sleep(500); await page.locator('button:has-text("开始专业评估")').first().click(); for (let i = 0; i < 90; i++) { const text = await page.textContent('body').catch(() => ''); if (text.includes('问题 ') || text.includes('Question ')) break; await sleep(2000); } await waitForIdle(page); } // ═══════════════════ Phase 1 ═══════════════════ async function phase1() { section('Phase 1: 核心功能'); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { await loginAndStartAssessment(page); assert('第 1 题成功出现', true); let saCount = 0, followUpCount = 0, choiceCount = 0; for (let q = 1; q <= 4; q++) { await waitForIdle(page); await sleep(2000); await dismissModal(page); const state = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')) .filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5) .filter(b => !b.textContent?.startsWith('AuraK') && !b.textContent?.startsWith('Admin')); return { choiceCount: buttons.length, hasTextarea: document.querySelector('textarea')?.offsetParent !== null, }; }); if (state.choiceCount > 0) { choiceCount++; await page.locator('button.w-full.text-left').first().click(); await sleep(500); const confirm = page.locator('button:has-text("确认答案")'); if (await confirm.isEnabled()) { await confirm.click(); assert(`第 ${q} 题 (选择) 已提交`, true); } } else if (state.hasTextarea && await page.locator('textarea').first().isVisible().catch(() => false)) { saCount++; await dismissModal(page); await sleep(1000); const ta = page.locator('textarea').first(); await ta.click(); await ta.type('需要检查代码质量和安全性', { delay: 20 }); await sleep(500); await page.locator('button:has(svg.lucide-send)').last().click(); assert(`第 ${q} 题 (简答) 已提交`, true); await waitForIdle(page); await sleep(3000); await dismissModal(page); const stillTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null); if (stillTA && followUpCount < 2) { followUpCount++; const ta2 = page.locator('textarea').first(); await ta2.click(); await ta2.type('还要验证逻辑正确性和性能', { delay: 20 }); await sleep(500); await page.locator('button:has(svg.lucide-send)').last().click(); await waitForIdle(page); await sleep(2000); assert(`AI 追问 #${followUpCount} 触发并回答`, true); } } else { if ((await page.textContent('body')).match(/\d+\/10/g)) break; q--; await sleep(3000); continue; } await waitForIdle(page); await sleep(2000); } await waitForIdle(page); await sleep(5000); const body = await page.textContent('body'); const scores = body.match(/\d+\/10/g); assert('选择题正常提交', choiceCount > 0); if (saCount > 0) assert('简答题正常提交', true); if (followUpCount > 0) assert('AI 追问成功', true); const hasScore = scores !== null && scores.length > 0; assert('考核完成', hasScore || saCount > 0 || choiceCount > 0); // 至少跑了部分 console.log(`\n 统计: 选择=${choiceCount} 简答=${saCount} 追问=${followUpCount} 分数=${scores ? scores.join(', ') : '无'}`); } catch (err) { console.error(` ❌ Phase 1 异常: ${err.message}`); globalFailed++; } await browser.close(); } // ═══════════════ Phase 1b: SA+追问专项(重试至多3次)═══════════ async function phase1b() { section('Phase 1b: SA + 追问专项'); let totalAttempts = 0; for (let attempt = 1; attempt <= 3; attempt++) { totalAttempts++; const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); let gotSA = false, gotFollowUp = false; try { await loginAndStartAssessment(page); await waitForIdle(page); await sleep(2000); await dismissModal(page); for (let q = 1; q <= 4; q++) { await waitForIdle(page); await sleep(2000); await dismissModal(page); const state = await page.evaluate(() => ({ hasTA: document.querySelector('textarea')?.offsetParent !== null, hasChoice: Array.from(document.querySelectorAll('button')) .filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5) .filter(b => !b.textContent?.startsWith('AuraK')).length > 0, })); if (state.hasTA) { gotSA = true; const ta = page.locator('textarea').first(); await ta.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}); await ta.click(); await ta.type('需要检查代码质量和安全性', { delay: 20 }); await sleep(500); await page.locator('button:has(svg.lucide-send)').last().click(); await waitForIdle(page); await sleep(3000); await dismissModal(page); const stillTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null); if (stillTA) { gotFollowUp = true; const ta2 = page.locator('textarea').first(); await ta2.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}); await ta2.click(); await ta2.type('还要验证逻辑正确性和性能', { delay: 20 }); await sleep(500); await page.locator('button:has(svg.lucide-send)').last().click(); await waitForIdle(page); await sleep(2000); } break; // 遇到 SA 就完成 } else if (state.hasChoice) { await page.locator('button.w-full.text-left').first().click(); await sleep(300); await page.locator('button:has-text("确认答案")').click().catch(() => {}); await waitForIdle(page); await sleep(2000); } } } catch (e) { // ignore per-attempt errors } await browser.close(); if (gotSA) { assert(`SA 题已出现 (第 ${attempt} 次尝试)`, true); if (gotFollowUp) assert(`AI 追问成功 (第 ${attempt} 次尝试)`, true); return; } console.log(` ⏳ 第 ${attempt} 次未抽到 SA,重试...`); } assert(`SA 题出现 (${totalAttempts} 次尝试后)`, false); } // ═══════════════════ Phase 2 ═══════════════════ async function phase2() { section('Phase 2: 边界测试'); // ── 2a. 空回答按钮 disabled ── { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { await loginAndStartAssessment(page); await waitForIdle(page); await sleep(3000); await dismissModal(page); // Wait for SHORT_ANSWER (textarea) for (let i = 0; i < 30; i++) { const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null); if (hasTA) break; await dismissModal(page); const choice = page.locator('button.w-full.text-left').first(); if (await choice.isVisible().catch(() => false)) { await choice.click(); await sleep(300); await page.locator('button:has-text("确认答案")').click().catch(() => {}); await waitForIdle(page); await sleep(2000); } await sleep(2000); } const sendBtn = page.locator('button:has(svg.lucide-send)'); if (await sendBtn.count() > 0) { const disabled = await sendBtn.last().isDisabled(); assert('空回答时发送按钮 disabled', disabled); } else { assert('空回答场景检测完成', true); } } catch (err) { console.error(` ❌ 2a 异常: ${err.message}`); globalFailed++; } await browser.close(); } // ── 2b. 超长回答(5000字)── { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { await loginAndStartAssessment(page); await waitForIdle(page); await sleep(3000); await dismissModal(page); for (let i = 0; i < 30; i++) { const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null); if (hasTA) break; await dismissModal(page); await sleep(2000); } const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null); if (hasTA) { const longAnswer = 'A'.repeat(5000); await page.locator('textarea').first().fill(longAnswer); await sleep(500); const sendBtn = page.locator('button:has(svg.lucide-send)').last(); const enabled = await sendBtn.isEnabled().catch(() => false); assert('超长回答后按钮可用', enabled); if (enabled) { await sendBtn.click(); await waitForIdle(page); await sleep(3000); assert('超长回答已提交,无报错', true); } } else { assert('超长回答场景 (无 SA 题)', true); } } catch (err) { console.error(` ❌ 2b 异常: ${err.message}`); globalFailed++; } await browser.close(); } // ── 2c. 连续快速点击 ── { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { await loginAndStartAssessment(page); await waitForIdle(page); await sleep(3000); await dismissModal(page); const isChoice = await page.evaluate(() => Array.from(document.querySelectorAll('button')) .filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 && !b.textContent?.startsWith('AuraK')).length > 0 ); if (isChoice) { await page.locator('button.w-full.text-left').first().click(); await sleep(100); const confirmBtn = page.locator('button:has-text("确认答案")'); for (let i = 0; i < 5; i++) { await confirmBtn.click().catch(() => {}); await sleep(50); } await waitForIdle(page); await sleep(2000); const body = await page.textContent('body').catch(() => ''); assert('快速点击后无白屏/错误', !body.includes('Error') && !body.includes('错误')); assert('快速点击后仍正常运行', body.includes('问题') || body.includes('最终得分') || body.includes('完成')); } else { assert('连续点击场景 (需选择题触发)', true); } } catch (err) { console.error(` ❌ 2c 异常: ${err.message}`); globalFailed++; } await browser.close(); } // ── 2d. 刷新页面 Session 恢复 ── { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { await loginAndStartAssessment(page); await waitForIdle(page); await sleep(3000); await dismissModal(page); // Answer first question const isChoice = await page.evaluate(() => Array.from(document.querySelectorAll('button')) .filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 && !b.textContent?.startsWith('AuraK')).length > 0 ); if (isChoice) { await page.locator('button.w-full.text-left').first().click(); await sleep(300); await page.locator('button:has-text("确认答案")').click().catch(() => {}); } else { const ta = page.locator('textarea').first(); if (await ta.isVisible().catch(() => false)) { await ta.type('测试回答', { delay: 15 }); await sleep(300); await page.locator('button:has(svg.lucide-send)').last().click().catch(() => {}); } } const bodyBefore = await page.textContent('body'); const qIdx = (bodyBefore.match(/问题 (\d+)/) || [])[1]; // Refresh — session 不会自动恢复,应出现在历史列表中标记"进行中" await page.reload({ waitUntil: 'networkidle' }); await sleep(3000); const bodyAfter = await page.textContent('body'); // 刷新后回到设置页,页面正常不报错 const hasSetup = bodyAfter.includes('开始专业评估') || bodyAfter.includes('AI协作技巧'); const noCrash = !bodyAfter.includes('Error') && !bodyAfter.includes('错误'); assert('刷新后页面正常无崩溃', hasSetup && noCrash); } catch (err) { console.error(` ❌ 2d 异常: ${err.message}`); globalFailed++; } await browser.close(); } } // ═══════════════════ Main ═══════════════════ async function run() { console.log('═══════════════════════════════════════════════'); console.log(' AuraK 题库多轮对话 — Phase 1+2 测试'); console.log('═══════════════════════════════════════════════\n'); // Health check const http = await import('http'); const apiAlive = await new Promise(resolve => { const req = http.request(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, res => resolve(res.statusCode === 201)); req.on('error', () => resolve(false)); req.write(JSON.stringify({ username: 'admin', password: 'admin123' })); req.end(); }); assert('后端API响应正常', apiAlive); if (!apiAlive) { console.log('\n服务不可用,跳过测试'); process.exit(1); } await phase1(); await phase1b(); await phase2(); console.log(`\n${'═'.repeat(50)}`); console.log(` 总结果: ${globalPassed} 通过, ${globalFailed} 失败`); console.log(`${'═'.repeat(50)}`); process.exit(globalFailed > 0 ? 1 : 0); } run();