/** * 体质分析功能自动化测试脚本 * 测试流程:登录 → 进入体质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' }); // 先滚动到按钮位置,确保它在视口内 const scrolled = 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) { el.scrollIntoView({ behavior: 'instant', block: 'center' }); return true; } } } return false; }); if (!scrolled) { console.log(' 未找到按钮'); logTest('进入测试页面', false, '未找到开始测试/重新测评按钮'); return false; } await page.waitForTimeout(800); // 再次获取按钮位置(滚动后坐标会变化) 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(); // 确保坐标在视口内 if (rect.y > 0 && rect.y < window.innerHeight) { 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; } // 只要有进度显示就认为成功进入测试页面 const success = progressText || backBtn; logTest('进入测试页面', success); return success; } 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】提交测试...'); // 截图当前状态 await page.screenshot({ path: 'tests/screenshots/before-submit.png' }); // 使用坐标点击方式找到提交按钮 const submitBtnPos = await page.evaluate(() => { const allElements = document.querySelectorAll('*'); for (const el of allElements) { const text = el.textContent?.trim(); if (text === '提交' && el.children.length === 0) { el.scrollIntoView({ behavior: 'instant', block: 'center' }); const rect = el.getBoundingClientRect(); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; } } return null; }); if (submitBtnPos) { console.log(` 找到提交按钮 at (${submitBtnPos.x}, ${submitBtnPos.y})`); await page.waitForTimeout(500); await page.mouse.click(submitBtnPos.x, submitBtnPos.y); console.log(' 已点击提交按钮,等待结果...'); await page.waitForTimeout(5000); // 截图提交后状态 await page.screenshot({ path: 'tests/screenshots/after-submit.png' }); // 验证跳转到结果页面 - 检查多种标识 const resultIndicators = [ 'text=体质分析报告', 'text=您的主体质倾向', 'text=体质得分', 'text=/平和质|气虚质|阳虚质|阴虚质|痰湿质|湿热质|血瘀质|气郁质|特禀质/' ]; let resultPage = false; for (const selector of resultIndicators) { const visible = await page.locator(selector).first().isVisible({ timeout: 3000 }).catch(() => false); if (visible) { console.log(` 找到结果页面标识: ${selector}`); resultPage = true; break; } } logTest('提交并查看结果', resultPage); return resultPage; } console.log(' 未找到提交按钮'); logTest('提交并查看结果', false, '未找到提交按钮'); return false; } async function verifyResult(page) { console.log('\n【步骤6】验证结果页面内容...'); // 截图保存 await page.screenshot({ path: 'tests/screenshots/constitution-result.png' }); // 使用 evaluate 方式检查页面文本内容 const pageContent = await page.evaluate(() => { const body = document.body.innerText || ''; return body; }); // 验证关键内容 const checks = [ { name: '体质分析报告标题', keyword: '体质分析报告' }, { name: '主体质名称', keyword: /(平和质|气虚质|阳虚质|阴虚质|痰湿质|湿热质|血瘀质|气郁质|特禀质)/ }, { name: '体质得分卡片', keyword: '体质得分' }, { name: '体质特征卡片', keyword: '体质特征' }, { name: '调理建议卡片', keyword: '调理建议' }, { name: '咨询AI助手按钮', keyword: /(咨询.*助手|AI助手)/ }, { name: '重新测评按钮', keyword: '重新测评' } ]; for (const check of checks) { let found = false; if (check.keyword instanceof RegExp) { found = check.keyword.test(pageContent); } else { found = pageContent.includes(check.keyword); } logTest(check.name, found); } // 获取体质类型 const typeMatch = pageContent.match(/(平和质|气虚质|阳虚质|阴虚质|痰湿质|湿热质|血瘀质|气郁质|特禀质)/); const typeText = typeMatch ? typeMatch[1] : '未知'; 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();