import { chromium } from 'playwright'; const BASE = 'http://localhost:13001'; const SA_REPLIES = [ '需要审查代码质量和安全性', // #1 代码审查 '还要检查逻辑正确性和性能问题', // #2 代码质量 '用清晰的提示词告诉AI具体需求', // #3 prompt技巧 '要注意数据安全和隐私保护', // #4 安全 'AI协作时要明确分工,人做决策', // #5 协作 '检查AI生成的内容是否准确', // #6 验证输出 '要测试边界情况和异常处理', // #7 测试 '把大任务拆成小步骤和AI沟通', // #8 任务拆分 '持续学习和更新对AI工具的认识', // #9 学习成长 '评估成本效益,选最合适的方案', // #10 综合 ]; /** Fill a textarea via native setter + input event (reliable for React controlled inputs) */ async function fillTextarea(page, text) { await page.evaluate((t) => { const ta = document.querySelector('textarea'); if (!ta) return; const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set; setter?.call(ta, t); ta.dispatchEvent(new Event('input', { bubbles: true })); }, text); await new Promise(r => setTimeout(r, 300)); } /** Click the send button (wait for it to be enabled, then regular or force click) */ async function clickSendButton(page) { try { await page.waitForFunction(() => { const btn = document.querySelector('button:has(svg.lucide-send)'); return btn && !btn.disabled && btn.offsetParent !== null; }, { timeout: 15000 }); await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 }); } catch { await new Promise(r => setTimeout(r, 1000)); await page.locator('button:has(svg.lucide-send)').last().click({ force: true, timeout: 5000 }).catch(() => {}); } } /** Wait for spinner to disappear */ async function waitForIdle(page) { await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 90000 }).catch(() => {}); await new Promise(r => setTimeout(r, 1500)); } async function run() { console.log('=== AuraK 10题考核多轮对话测试 ===\n'); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); // Login console.log('[1] 登录...'); await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); 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('**/'); console.log(' ✅ 登录成功'); // Assessment page await page.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // Select template await page.locator('button:has-text("AI协作技巧")').first().click(); await page.waitForTimeout(500); await page.locator('button:has-text("开始专业评估")').first().click(); // Wait for first question console.log('[2] 等待出题...'); for (let i = 0; i < 90; i++) { const text = await page.textContent('body').catch(() => ''); if (text.includes('问题 ') || text.includes('Question ')) break; await new Promise(r => setTimeout(r, 2000)); } console.log(' ✅ 第 1 题已出现'); await waitForIdle(page); // Answer questions let qIdx = 1; let saCount = 0, followUpCount = 0; const totalQs = 10; const startTime = Date.now(); // Per-question timeout: 5 minutes (AI generation can be slow) const Q_TIMEOUT = 300; // 5 min in seconds while (qIdx <= totalQs) { // Detect question type const state = await page.evaluate(() => { const allBtns = Array.from(document.querySelectorAll('button')); const optionBtns = allBtns.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 && !(b.textContent || '').includes('AuraK') && !(b.textContent || '').includes('Admin') ); const confirmBtn = allBtns.find(b => (b.textContent || '').includes('确认答案')); const ta = document.querySelector('textarea'); return { choiceCount: optionBtns.length, hasTextarea: ta !== null && ta.offsetParent !== null, firstChoice: optionBtns[0]?.textContent?.trim().substring(0, 40) || '', hasConfirmBtn: confirmBtn !== null, busy: document.querySelector('.animate-spin') !== null, hasQuestion: (document.body.textContent || '').includes('问题 ') || (document.body.textContent || '').includes('Question '), }; }); // If busy (spinner visible), wait and retry if (state.busy) { console.log(` ⏳ AI正在处理...`); await new Promise(r => setTimeout(r, 3000)); continue; } // If neither question type detected but question text exists, wait more if (state.choiceCount === 0 && !state.hasTextarea && state.hasQuestion) { console.log(` ⏳ 题型未就绪,等待...`); await new Promise(r => setTimeout(r, 3000)); continue; } // Read current question text const questionText = await page.evaluate(() => { const bubbles = Array.from(document.querySelectorAll('.px-5.py-4')); for (let i = bubbles.length - 1; i >= 0; i--) { const el = bubbles[i]; const text = el.textContent || ''; if (text.length > 25 && !(el.getAttribute('class') || '').includes('bg-indigo')) { return text.substring(0, 160); } } return ''; }); if (state.choiceCount > 0) { // ── CHOICE ── const qShort = questionText.replace(/\s+/g, ' ').substring(0, 100); console.log(`\n 🟦 第 ${qIdx}/${totalQs} 题 (选择) "${qShort}..."`); const btns = page.locator('button.w-full.text-left.px-5.py-4'); const count = await btns.count(); if (count > 0) { await btns.first().click(); await new Promise(r => setTimeout(r, 500)); } if (state.hasConfirmBtn) { await page.locator('button:has-text("确认答案")').click(); console.log(` ✅ 已提交`); } qIdx++; // After submitting a choice, wait for transition await waitForIdle(page); } else if (state.hasTextarea) { // ── SHORT ANSWER ── saCount++; const replyIdx = Math.min(saCount - 1, SA_REPLIES.length - 1); console.log(`\n 🟩 第 ${qIdx}/${totalQs} 题 (简答 #${saCount})`); await page.locator('textarea').first().click(); await fillTextarea(page, SA_REPLIES[replyIdx]); console.log(` 📝 已输入: "${SA_REPLIES[replyIdx].substring(0, 20)}..."`); await clickSendButton(page); console.log(` ✅ 已提交`); // Wait for grading await waitForIdle(page); // Check for follow-up question const stillTA = await page.evaluate(() => { const ta = document.querySelector('textarea'); return ta !== null && ta.offsetParent !== null; }); if (stillTA) { followUpCount++; const fReply = SA_REPLIES[Math.min(followUpCount, SA_REPLIES.length - 1)]; console.log(` 🔄 AI 追问 #${followUpCount} 已触发!`); await page.locator('textarea').first().click(); await fillTextarea(page, fReply); console.log(` 📝 追问已输入: "${fReply.substring(0, 20)}..."`); await clickSendButton(page); console.log(` ✅ 追问已提交`); await waitForIdle(page); } qIdx++; } else { // ── WAITING for question to appear ── // Check for question text in body const bodyText = await page.textContent('body').catch(() => ''); if (bodyText.includes('问题 ') || bodyText.includes('Question ')) { // Question is there but types not detected yet - wait for spinner then retry console.log(` ⏳ 第 ${qIdx} 题文本已见,等待组件渲染...`); await waitForIdle(page); continue; } // Check if assessment completed if (bodyText.includes('合格') || bodyText.includes('VERIFIED') || bodyText.includes('LEVEL')) { console.log(`\n 📋 考核已完成!`); break; } // Check per-question timeout const elapsed = Math.round((Date.now() - startTime) / 1000); if (elapsed > Q_TIMEOUT * qIdx + 120) { console.log(` ⏰ 第 ${qIdx} 题生成超时,跳过`); qIdx++; continue; } console.log(` ⏳ 等待 AI 生成第 ${qIdx} 题...`); await waitForIdle(page); await new Promise(r => setTimeout(r, 5000)); continue; } } // Wait for results page console.log('\n ⏳ 等待考核结果完成...'); await waitForIdle(page); await new Promise(r => setTimeout(r, 5000)); const elapsed = Math.round((Date.now() - startTime) / 1000); const body = await page.textContent('body'); const scores = body.match(/\d+\/10/g); const level = body.match(/LEVEL:\s*(\w+)/i)?.[1] || body.match(/等级[::]\s*(\w+)/)?.[1] || '?'; const passed = body.includes('合格') || body.includes('VERIFIED'); console.log(`\n 📊 结果 (耗时 ${Math.floor(elapsed/60)}分${elapsed%60}秒):`); console.log(` 总题数: ${totalQs}`); console.log(` 选择题: ${totalQs - saCount}`); console.log(` 简答题: ${saCount}`); console.log(` AI追问: ${followUpCount}次`); console.log(` 分数: ${scores ? scores.join(', ') : '无'}`); console.log(` 等级: ${level}`); console.log(` ${passed ? '🎉 合格!' : '😅 未合格'}`); if (followUpCount > 0) { console.log(`\n 🎉 多轮对话正常工作!`); } else if (saCount > 0) { console.log(`\n ✅ 简答题正常回答,未触发追问(回答已完整)。`); } else { console.log(`\n ⚠️ 未遇到简答题,需要确认 shuffle 是否生效。`); } await page.screenshot({ path: 'assessment-10q-result.png', fullPage: true }); console.log(' 📸 截图已保存'); await browser.close(); console.log('\n=== 完成 ==='); } run().catch(e => { console.error('\n❌', e.message); process.exit(1); });