You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
16 KiB
460 lines
16 KiB
/**
|
|
* 体质分析功能自动化测试脚本
|
|
* 测试流程:登录 → 进入体质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();
|
|
|