/** * 体质分析功能自动化测试脚本 * 测试流程:登录 → 进入体质Tab → 开始测试 → 回答问题 → 提交 → 查看结果 */ const { chromium } = require('playwright'); const APP_URL = 'http://localhost:8081'; const TEST_PHONE = '13800138000'; const TEST_CODE = '123456'; // 测试结果统计 const testResults = { passed: 0, failed: 0, tests: [] }; function logTest(name, passed, detail = '') { const status = passed ? '✓' : '✗'; const msg = `${status} ${name}${detail ? ': ' + detail : ''}`; console.log(msg); testResults.tests.push({ name, passed, detail }); if (passed) testResults.passed++; else testResults.failed++; } async function login(page) { console.log('\n【步骤1】检查登录状态...'); const loginBtn = page.locator('text=登录').first(); if (!(await loginBtn.isVisible({ timeout: 2000 }).catch(() => false))) { logTest('已登录状态', true, '跳过登录流程'); return true; } console.log(' 执行登录流程...'); // 输入手机号 const phoneInput = page.locator('input').first(); await phoneInput.fill(TEST_PHONE); await page.waitForTimeout(300); // 获取验证码 const getCodeBtn = page.locator('text=获取验证码').first(); if (await getCodeBtn.isVisible()) { await getCodeBtn.click(); await page.waitForTimeout(1000); } // 输入验证码 const codeInput = page.locator('input').nth(1); await codeInput.fill(TEST_CODE); await page.waitForTimeout(300); // 点击登录 await loginBtn.click(); await page.waitForTimeout(3000); // 验证登录成功 const homeVisible = await page.locator('text=/.*好,.*$/').first().isVisible({ timeout: 5000 }).catch(() => false); logTest('登录', homeVisible); return homeVisible; } async function navigateToConstitution(page) { console.log('\n【步骤2】导航到体质Tab...'); // 点击体质Tab const constitutionTab = page.locator('text=体质').first(); if (await constitutionTab.isVisible()) { await constitutionTab.click(); await page.waitForTimeout(1500); // 验证进入体质首页 const pageTitle = await page.locator('text=体质分析').first().isVisible({ timeout: 3000 }).catch(() => false); logTest('导航到体质页面', pageTitle); return pageTitle; } logTest('导航到体质页面', false, '未找到体质Tab'); return false; } async function startTest(page) { console.log('\n【步骤3】开始体质测试...'); // 截图当前状态 await page.screenshot({ path: 'tests/screenshots/before-start.png' }); // 滚动到页面底部确保按钮可见 await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(500); // 获取按钮元素位置并使用鼠标点击 const btnBox = await page.evaluate(() => { const allElements = document.querySelectorAll('*'); for (const el of allElements) { const text = el.textContent?.trim(); if (text === '开始测试' || text === '重新测评') { // 确保是按钮内的文本元素 if (el.tagName === 'DIV' && el.children.length === 0) { const rect = el.getBoundingClientRect(); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, text: text }; } } } return null; }); if (btnBox) { console.log(` 找到按钮: ${btnBox.text} at (${btnBox.x}, ${btnBox.y})`); // 使用鼠标点击坐标 await page.mouse.click(btnBox.x, btnBox.y); console.log(' 已执行鼠标点击,等待页面加载...'); await page.waitForTimeout(3000); } else { console.log(' 未找到按钮'); logTest('进入测试页面', false, '未找到开始测试按钮'); return false; } // 截图点击后状态 await page.screenshot({ path: 'tests/screenshots/after-start-click.png' }); // 验证进入测试页面 - 检查是否有"← 返回"按钮和进度条 const backBtn = await page.locator('text=← 返回').first().isVisible({ timeout: 3000 }).catch(() => false); const progressText = await page.locator('text=/第 \\d+ 题 \\/ 共 \\d+ 题/').first().isVisible({ timeout: 5000 }).catch(() => false); const loadingText = await page.locator('text=加载题目中').first().isVisible({ timeout: 1000 }).catch(() => false); console.log(` 返回按钮: ${backBtn}, 进度显示: ${progressText}, 加载中: ${loadingText}`); // 如果显示加载中,等待更长时间 if (loadingText) { console.log(' 等待题目加载完成...'); await page.waitForTimeout(8000); await page.screenshot({ path: 'tests/screenshots/after-loading.png' }); const progressNow = await page.locator('text=/第 \\d+ 题 \\/ 共 \\d+ 题/').first().isVisible({ timeout: 5000 }).catch(() => false); logTest('进入测试页面', progressNow); return progressNow; } logTest('进入测试页面', backBtn && progressText); return backBtn && progressText; } async function answerQuestions(page) { console.log('\n【步骤4】回答问题...'); let questionCount = 0; let maxQuestions = 70; // 安全上限(实际67题) while (questionCount < maxQuestions) { questionCount++; // 获取当前题号信息 const progressText = await page.locator('text=/第 \\d+ 题 \\/ 共 \\d+ 题/').first().textContent().catch(() => ''); const match = progressText.match(/第 (\d+) 题 \/ 共 (\d+) 题/); if (match) { const current = parseInt(match[1]); const total = parseInt(match[2]); console.log(` 回答第 ${current}/${total} 题...`); // 使用坐标点击选项 - 更可靠的方式 const optionClicked = await page.evaluate(() => { const optionTexts = ['没有', '很少', '有时', '经常', '总是']; const randomText = optionTexts[Math.floor(Math.random() * optionTexts.length)]; // 查找包含选项文本的元素 const allElements = document.querySelectorAll('*'); for (const el of allElements) { if (el.textContent?.trim() === randomText && el.children.length === 0) { const rect = el.getBoundingClientRect(); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, text: randomText }; } } // 备用:点击第一个选项区域(查找选项卡片) const cards = document.querySelectorAll('[style*="border"][style*="padding"]'); for (const card of cards) { const rect = card.getBoundingClientRect(); if (rect.width > 100 && rect.height > 30) { return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, text: 'card' }; } } return null; }); if (optionClicked) { await page.mouse.click(optionClicked.x, optionClicked.y); await page.waitForTimeout(500); } // 检查是否是最后一题 if (current === total) { // 最后一题,点击提交 console.log(' 所有题目已回答,准备提交...'); logTest('回答所有问题', true, `共 ${total} 题`); return true; } else { // 点击下一题 - 使用坐标点击 const nextBtnPos = await page.evaluate(() => { const allElements = document.querySelectorAll('*'); for (const el of allElements) { if (el.textContent?.trim() === '下一题' && el.children.length === 0) { const rect = el.getBoundingClientRect(); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; } } return null; }); if (nextBtnPos) { await page.mouse.click(nextBtnPos.x, nextBtnPos.y); await page.waitForTimeout(600); } } } else { console.log(' 无法解析题号,尝试继续...'); await page.waitForTimeout(500); } } logTest('回答所有问题', false, '超过最大题目数量'); return false; } async function submitTest(page) { console.log('\n【步骤5】提交测试...'); const submitBtn = page.getByRole('button', { name: '提交' }).first(); if (await submitBtn.isVisible({ timeout: 3000 }).catch(() => false)) { await submitBtn.click({ force: true }); await page.waitForTimeout(3000); // 验证跳转到结果页面 const resultPage = await page.locator('text=体质分析报告').first().isVisible({ timeout: 8000 }).catch(() => false); logTest('提交并查看结果', resultPage); return resultPage; } logTest('提交并查看结果', false, '未找到提交按钮'); return false; } async function verifyResult(page) { console.log('\n【步骤6】验证结果页面内容...'); // 截图保存 await page.screenshot({ path: 'tests/screenshots/constitution-result.png' }); // 验证关键元素 const checks = [ { name: '体质分析报告标题', selector: 'text=体质分析报告' }, { name: '主体质名称', selector: 'text=/平和质|气虚质|阳虚质|阴虚质|痰湿质|湿热质|血瘀质|气郁质|特禀质/' }, { name: '体质得分卡片', selector: 'text=📊 体质得分' }, { name: '体质特征卡片', selector: 'text=📋 体质特征' }, { name: '调理建议卡片', selector: 'text=💡 调理建议' }, { name: '咨询AI助手按钮', selector: 'text=咨询AI助手' }, { name: '重新测评按钮', selector: 'text=重新测评' } ]; for (const check of checks) { const visible = await page.locator(check.selector).first().isVisible({ timeout: 2000 }).catch(() => false); logTest(check.name, visible); } // 获取体质类型 const typeText = await page.locator('text=/平和质|气虚质|阳虚质|阴虚质|痰湿质|湿热质|血瘀质|气郁质|特禀质/').first().textContent().catch(() => '未知'); console.log(`\n 检测到体质类型: ${typeText}`); return true; } async function testRetest(page) { console.log('\n【步骤7】测试重新测评功能...'); const retestBtn = page.getByRole('button', { name: '重新测评' }).first(); if (await retestBtn.isVisible({ timeout: 3000 }).catch(() => false)) { await retestBtn.click({ force: true }); await page.waitForTimeout(2000); // 验证返回测试页面 const backToTest = await page.locator('text=体质测试').first().isVisible({ timeout: 3000 }).catch(() => false); logTest('重新测评导航', backToTest); // 返回结果页面(为了完成测试) const backBtn = page.locator('text=← 返回').first(); if (await backBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await backBtn.click({ force: true }); await page.waitForTimeout(1000); } return backToTest; } logTest('重新测评导航', false, '未找到重新测评按钮'); return false; } async function runTests() { console.log('═══════════════════════════════════════════════════════════'); console.log(' 体质分析功能自动化测试'); console.log('═══════════════════════════════════════════════════════════'); const browser = await chromium.launch({ headless: false }); const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await context.newPage(); // 监听控制台日志 page.on('console', msg => { if (msg.type() === 'error') { console.log(' [Console Error]', msg.text()); } }); // 监听网络请求错误 page.on('requestfailed', request => { console.log(' [Request Failed]', request.url(), request.failure().errorText); }); // 监听页面错误 page.on('pageerror', error => { console.log(' [Page Error]', error.message); }); try { console.log('\n打开应用...'); await page.goto(APP_URL); await page.waitForTimeout(2000); // 执行测试步骤 const loginOk = await login(page); if (!loginOk) throw new Error('登录失败'); const navOk = await navigateToConstitution(page); if (!navOk) throw new Error('导航失败'); const startOk = await startTest(page); if (!startOk) throw new Error('无法开始测试'); const answerOk = await answerQuestions(page); if (!answerOk) throw new Error('回答问题失败'); const submitOk = await submitTest(page); if (!submitOk) throw new Error('提交失败'); await verifyResult(page); await testRetest(page); } catch (error) { console.error('\n测试中断:', error.message); await page.screenshot({ path: 'tests/screenshots/constitution-error.png' }); } finally { // 打印测试摘要 console.log('\n═══════════════════════════════════════════════════════════'); console.log(' 测试结果摘要'); console.log('═══════════════════════════════════════════════════════════'); console.log(`通过: ${testResults.passed} 失败: ${testResults.failed}`); console.log('───────────────────────────────────────────────────────────'); for (const test of testResults.tests) { const icon = test.passed ? '✓' : '✗'; console.log(`${icon} ${test.name}${test.detail ? ' - ' + test.detail : ''}`); } console.log('═══════════════════════════════════════════════════════════'); await page.waitForTimeout(3000); await browser.close(); // 返回退出码 process.exit(testResults.failed > 0 ? 1 : 0); } } // 运行测试 runTests();