Browse Source

test: 添加 Playwright 自动化测试脚本

- constitution.test.js: 体质分析功能测试
- profile.test.js: "我的"页面功能测试
- health-profile-complete.test.js: 健康档案完整测试
- 包含测试使用文档 README.md

Co-authored-by: Cursor <cursoragent@cursor.com>
master
dark 2 days ago
parent
commit
58e71ebcef
  1. 476
      tests/README.md
  2. 387
      tests/constitution.test.js
  3. 1229
      tests/health-profile-complete.test.js
  4. 1075
      tests/profile.test.js
  5. BIN
      tests/screenshots/constitution-result.png
  6. BIN
      tests/screenshots/health-profile-add-allergy.png
  7. BIN
      tests/screenshots/health-profile-add-medical.png
  8. BIN
      tests/screenshots/health-profile-cards.png
  9. BIN
      tests/screenshots/health-profile-edit-basic.png
  10. BIN
      tests/screenshots/health-profile-edit-lifestyle.png
  11. BIN
      tests/screenshots/health-profile-final.png
  12. BIN
      tests/screenshots/health-profile-page.png
  13. BIN
      tests/screenshots/hp-allergy-add-after.png
  14. BIN
      tests/screenshots/hp-allergy-add-before.png
  15. BIN
      tests/screenshots/hp-allergy-saved.png
  16. BIN
      tests/screenshots/hp-basic-edit-after.png
  17. BIN
      tests/screenshots/hp-basic-edit-before.png
  18. BIN
      tests/screenshots/hp-basic-saved.png
  19. BIN
      tests/screenshots/hp-family-add-after.png
  20. BIN
      tests/screenshots/hp-family-add-before.png
  21. BIN
      tests/screenshots/hp-family-saved.png
  22. BIN
      tests/screenshots/hp-final-verification.png
  23. BIN
      tests/screenshots/hp-initial.png
  24. BIN
      tests/screenshots/hp-lifestyle-edit-after.png
  25. BIN
      tests/screenshots/hp-lifestyle-edit-before.png
  26. BIN
      tests/screenshots/hp-lifestyle-saved.png
  27. BIN
      tests/screenshots/hp-medical-add-after.png
  28. BIN
      tests/screenshots/hp-medical-add-before.png
  29. BIN
      tests/screenshots/hp-medical-delete-confirm.png
  30. BIN
      tests/screenshots/hp-medical-delete-done.png
  31. BIN
      tests/screenshots/hp-medical-saved.png
  32. BIN
      tests/screenshots/profile-about-dialog.png
  33. BIN
      tests/screenshots/profile-edit-modal.png
  34. BIN
      tests/screenshots/profile-health-profile.png
  35. BIN
      tests/screenshots/profile-logout-confirm.png
  36. BIN
      tests/screenshots/profile-medication-modal.png
  37. BIN
      tests/screenshots/profile-page.png

476
tests/README.md

@ -0,0 +1,476 @@
# 自动化测试文档
本目录包含基于 Playwright 的端到端 (E2E) 自动化测试脚本。
## 目录结构
```
tests/
├── README.md # 本文档
├── constitution.test.js # 体质分析功能测试
├── profile.test.js # "我的"页面功能测试
├── health-profile-complete.test.js # 健康档案完整功能测试(推荐)
└── screenshots/ # 测试截图目录
├── constitution-result.png # 体质测试结果截图
├── profile-page.png # 我的页面截图
├── hp-basic-*.png # 基础信息编辑截图
├── hp-lifestyle-*.png # 生活习惯编辑截图
├── hp-medical-*.png # 病史记录添加截图
└── hp-allergy-*.png # 过敏记录添加截图
```
## 环境准备
### 1. 安装依赖
```bash
# 安装 Playwright
npm install playwright
# 安装浏览器(首次运行)
npx playwright install chromium
```
### 2. 启动应用
测试前需要确保前端和后端服务都在运行:
```bash
# 终端1: 启动后端服务
cd server
go run main.go
# 终端2: 启动前端服务
cd app
npm start
# 或
npx expo start --web
```
确保应用可以通过 `http://localhost:8081` 访问。
## 运行测试
### 运行所有测试
```bash
# 从项目根目录运行
node tests/constitution.test.js # 体质分析测试
node tests/profile.test.js # "我的"页面测试
node tests/health-profile-complete.test.js # 健康档案完整测试(推荐)
```
### 运行体质分析测试
```bash
node tests/constitution.test.js
```
### 运行"我的"页面测试
```bash
node tests/profile.test.js
```
### 运行健康档案完整测试(推荐)
```bash
node tests/health-profile-complete.test.js
```
### 测试配置
测试脚本中的配置项(位于文件开头):
```javascript
const APP_URL = "http://localhost:8081"; // 应用地址
const TEST_PHONE = "13800138000"; // 测试手机号
const TEST_CODE = "123456"; // 测试验证码
```
## 测试脚本说明
### constitution.test.js - 体质分析功能测试
**测试流程:**
1. **登录** - 使用测试账号登录应用
2. **导航** - 进入"体质"Tab
3. **开始测试** - 点击"开始测试"按钮
4. **回答问题** - 自动回答 67 道体质问卷题目
5. **提交** - 提交答案获取结果
6. **验证结果** - 检查结果页面各元素
7. **重新测评** - 测试重新测评功能
**验证项目:**
| 检查项 | 说明 |
| ---------------- | ---------------------- |
| 登录 | 验证登录流程正常 |
| 导航到体质页面 | 验证 Tab 导航正常 |
| 进入测试页面 | 验证开始测试按钮可点击 |
| 回答所有问题 | 验证 67 道题目全部完成 |
| 提交并查看结果 | 验证提交后跳转到结果页 |
| 体质分析报告标题 | 验证结果页标题显示 |
| 主体质名称 | 验证显示体质类型名称 |
| 体质得分卡片 | 验证得分区域显示 |
| 体质特征卡片 | 验证特征区域显示 |
| 调理建议卡片 | 验证建议区域显示 |
| 咨询 AI 助手按钮 | 验证功能按钮显示 |
| 重新测评按钮 | 验证重新测评按钮显示 |
| 重新测评导航 | 验证重新测评功能正常 |
**输出示例:**
```
═══════════════════════════════════════════════════════════
体质分析功能自动化测试
═══════════════════════════════════════════════════════════
打开应用...
【步骤1】检查登录状态...
执行登录流程...
✓ 登录
【步骤2】导航到体质Tab...
✓ 导航到体质页面
【步骤3】开始体质测试...
✓ 进入测试页面
【步骤4】回答问题...
回答第 1/67 题...
回答第 2/67 题...
...
回答第 67/67 题...
所有题目已回答,准备提交...
✓ 回答所有问题: 共 67 题
【步骤5】提交测试...
✓ 提交并查看结果
【步骤6】验证结果页面内容...
✓ 体质分析报告标题
✓ 主体质名称
✓ 体质得分卡片
✓ 体质特征卡片
✓ 调理建议卡片
✓ 咨询AI助手按钮
✓ 重新测评按钮
检测到体质类型: 特禀质
【步骤7】测试重新测评功能...
✓ 重新测评导航
═══════════════════════════════════════════════════════════
测试结果摘要
═══════════════════════════════════════════════════════════
通过: 13 失败: 0
───────────────────────────────────────────────────────────
✓ 登录
✓ 导航到体质页面
✓ 进入测试页面
✓ 回答所有问题 - 共 67 题
✓ 提交并查看结果
✓ 体质分析报告标题
✓ 主体质名称
✓ 体质得分卡片
✓ 体质特征卡片
✓ 调理建议卡片
✓ 咨询AI助手按钮
✓ 重新测评按钮
✓ 重新测评导航
═══════════════════════════════════════════════════════════
```
### profile.test.js - "我的"页面功能测试
**测试流程:**
1. **登录** - 使用测试账号登录
2. **导航** - 进入"我的"Tab 页面
3. **用户信息显示** - 验证昵称、手机号、编辑按钮
4. **编辑昵称** - 打开弹窗、修改昵称、保存
5. **适老模式** - 验证开关功能
6. **健康管理菜单** - 验证四个菜单项显示
7. **健康档案导航** - 跳转到健康档案页面
8. **健康档案编辑功能** - 测试基础信息/生活习惯编辑、病史/家族病史/过敏记录新增
9. **用药/治疗记录** - 打开弹窗查看
10. **关于我们** - 打开弹窗查看
11. **退出登录** - 点击并验证确认弹窗
**验证项目:**
| 检查项 | 说明 |
| -------------------- | -------------------- |
| 登录 | 使用测试账号登录 |
| 导航到"我的"页面 | Tab 导航正常 |
| 用户昵称显示 | 显示用户昵称 |
| 手机号显示 | 显示手机号 |
| 编辑按钮显示 | 编辑图标可见 |
| 编辑弹窗打开 | 点击编辑打开弹窗 |
| 保存昵称 | 修改昵称并保存成功 |
| 适老模式卡片显示 | 适老模式开关可见 |
| 适老模式开关 | 开关可切换 |
| 健康档案菜单显示 | 健康档案菜单可见 |
| 用药记录菜单显示 | 用药记录菜单可见 |
| 体质报告菜单显示 | 体质报告菜单可见 |
| 对话历史菜单显示 | 对话历史菜单可见 |
| 健康档案页面打开 | 导航到健康档案页 |
| 基础信息卡片显示 | 基础信息卡片可见 |
| 基础信息编辑弹窗打开 | 点击编辑按钮打开弹窗 |
| 生活习惯卡片显示 | 生活习惯卡片可见 |
| 病史记录卡片显示 | 病史记录卡片可见 |
| 病史记录新增弹窗打开 | 点击新增按钮打开弹窗 |
| 家族病史卡片显示 | 家族病史卡片可见 |
| 过敏记录卡片显示 | 过敏记录卡片可见 |
| 用药记录弹窗打开 | 点击打开用药记录弹窗 |
| 退出登录按钮显示 | 退出按钮可见 |
**运行命令:**
```bash
node tests/profile.test.js
```
---
### health-profile-complete.test.js - 健康档案完整功能测试(推荐)
这是最全面的健康档案测试脚本,覆盖所有可编辑字段的输入和保存验证。
**测试范围:**
| 功能模块 | 测试字段数 | 测试内容 |
| -------- | ---------- | ------------------------------------------------------------------------------------------ |
| 基础信息 | 9 个字段 | 姓名、性别、出生日期、身高、体重、血型、职业、婚姻状况、地区 |
| 生活习惯 | 10 个字段 | 入睡时间、起床时间、睡眠质量、三餐规律、饮食偏好、日饮水量、运动频率、运动类型、吸烟、饮酒 |
| 病史记录 | 5 个字段 | 疾病名称、疾病类型、诊断日期、治疗状态、备注 |
| 家族病史 | 3 个字段 | 亲属关系、疾病名称、备注 |
| 过敏记录 | 4 个字段 | 过敏类型、过敏原、严重程度、过敏反应描述 |
**测试流程:**
1. **登录** - 使用测试账号登录
2. **导航** - 进入健康档案页面
3. **基础信息编辑** - 测试所有 9 个字段的输入和保存
4. **生活习惯编辑** - 测试所有 10 个字段的输入和保存
5. **病史记录添加** - 测试添加新病史记录(5 个字段)
6. **家族病史添加** - 测试添加新家族病史(3 个字段)
7. **过敏记录添加** - 测试添加新过敏记录(4 个字段)
8. **数据验证** - 刷新页面后验证所有数据是否正确保存
**验证项目:**
| 检查项 | 说明 |
| ----------------------- | ------------------------- |
| 导航到健康档案页面 | Tab 和菜单导航正常 |
| 打开基础信息编辑弹窗 | 编辑按钮可点击 |
| 基础信息-姓名输入 | TextInput 输入正常 |
| 基础信息-性别选择 | SegmentedButtons 选择正常 |
| 基础信息-出生日期输入 | 日期格式输入正常 |
| 基础信息-身高/体重输入 | 数字输入正常 |
| 基础信息-血型输入 | 文本输入正常 |
| 基础信息-职业输入 | 文本输入正常 |
| 基础信息-婚姻状况选择 | SegmentedButtons 选择正常 |
| 基础信息-地区输入 | 文本输入正常 |
| 基础信息-保存成功 | API 调用成功,显示提示 |
| 基础信息-保存后数据显示 | 页面正确显示保存的数据 |
| 生活习惯-所有字段测试 | 同上,共 10 个字段 |
| 病史记录-添加新记录 | 弹窗、输入、添加、显示 |
| 家族病史-添加新记录 | 弹窗、输入、添加、显示 |
| 过敏记录-添加新记录 | 弹窗、输入、添加、显示 |
| 刷新后数据验证 | 页面刷新后数据仍然正确 |
**输出示例:**
```
═══════════════════════════════════════════════════════════
健康档案完整功能自动化测试
═══════════════════════════════════════════════════════════
测试范围:
- 基础信息: 9个字段(姓名、性别、出生日期、身高、体重、血型、职业、婚姻、地区)
- 生活习惯: 10个字段(入睡时间、起床时间、睡眠质量、三餐规律、饮食偏好、
日饮水量、运动频率、运动类型、吸烟、饮酒)
- 病史记录: 5个字段(疾病名称、疾病类型、诊断日期、治疗状态、备注)
- 家族病史: 3个字段(亲属关系、疾病名称、备注)
- 过敏记录: 4个字段(过敏类型、过敏原、严重程度、过敏反应描述)
【步骤2】测试基础信息编辑(9个字段)...
✓ 打开基础信息编辑弹窗
✓ 基础信息-姓名输入
✓ 基础信息-性别选择
✓ 基础信息-出生日期输入
✓ 基础信息-身高输入
✓ 基础信息-体重输入
✓ 基础信息-血型输入
✓ 基础信息-职业输入
✓ 基础信息-婚姻状况选择
✓ 基础信息-地区输入
✓ 基础信息-保存成功 - 显示保存成功提示
✓ 基础信息-保存后姓名显示
✓ 基础信息-保存后地区显示
✓ 基础信息-保存后职业显示
... (更多测试输出)
═══════════════════════════════════════════════════════════
测试结果摘要
═══════════════════════════════════════════════════════════
通过: 58 失败: 0
═══════════════════════════════════════════════════════════
```
**运行命令:**
```bash
node tests/health-profile-complete.test.js
```
---
## 编写新测试
### 基础模板
```javascript
const { chromium } = require("playwright");
const APP_URL = "http://localhost:8081";
// 测试结果统计
const testResults = { passed: 0, failed: 0, tests: [] };
function logTest(name, passed, detail = "") {
const status = passed ? "✓" : "✗";
console.log(`${status} ${name}${detail ? ": " + detail : ""}`);
testResults.tests.push({ name, passed, detail });
if (passed) testResults.passed++;
else testResults.failed++;
}
async function runTests() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// 监听错误
page.on("console", (msg) => {
if (msg.type() === "error") {
console.log("[Console Error]", msg.text());
}
});
page.on("pageerror", (error) => {
console.log("[Page Error]", error.message);
});
try {
await page.goto(APP_URL);
await page.waitForTimeout(2000);
// 添加测试步骤...
} catch (error) {
console.error("测试中断:", error.message);
await page.screenshot({ path: "tests/screenshots/error.png" });
} finally {
// 打印结果
console.log(`\n通过: ${testResults.passed} 失败: ${testResults.failed}`);
await browser.close();
process.exit(testResults.failed > 0 ? 1 : 0);
}
}
runTests();
```
### 常用操作
#### 点击元素
```javascript
// 方式1: 文本定位
await page.locator("text=按钮文字").click();
// 方式2: 角色定位
await page.getByRole("button", { name: "提交" }).click();
// 方式3: 坐标点击 (适用于 React Native Web)
const pos = await page.evaluate(() => {
const el = document.querySelector("text=按钮");
const rect = el.getBoundingClientRect();
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
});
await page.mouse.click(pos.x, pos.y);
// 强制点击 (忽略可见性检查)
await page.locator("text=按钮").click({ force: true });
```
#### 输入文本
```javascript
const input = page.locator("input").first();
await input.fill("输入内容");
```
#### 等待元素
```javascript
// 等待元素可见
await page.locator("text=内容").waitFor({ state: "visible", timeout: 5000 });
// 检查元素是否可见
const visible = await page
.locator("text=内容")
.isVisible({ timeout: 2000 })
.catch(() => false);
```
#### 截图
```javascript
await page.screenshot({ path: "tests/screenshots/截图名.png" });
```
## 注意事项
### React Native Web 特殊处理
由于 React Native Web 的渲染机制,某些标准选择器可能不起作用:
1. **优先使用坐标点击** - 通过 `evaluate` 获取元素位置后用 `mouse.click`
2. **使用 force 选项** - 某些元素可能被遮挡,使用 `{ force: true }`
3. **增加等待时间** - React Native 动画可能需要更长时间完成
### 测试账号
测试使用的账号:
- 手机号: `13800138000`
- 验证码: `123456`
### 截图目录
测试截图保存在 `tests/screenshots/` 目录下,包括:
- 测试过程截图
- 错误截图(当测试失败时自动保存)
## CI/CD 集成
可以在 CI/CD 流程中使用 headless 模式运行测试:
```javascript
// 修改 browser launch 配置
const browser = await chromium.launch({
headless: true, // 无头模式
});
```
退出码说明:
- `0` - 所有测试通过
- `1` - 存在测试失败

387
tests/constitution.test.js

@ -0,0 +1,387 @@
/**
* 体质分析功能自动化测试脚本
* 测试流程登录 进入体质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();

1229
tests/health-profile-complete.test.js

File diff suppressed because it is too large

1075
tests/profile.test.js

File diff suppressed because it is too large

BIN
tests/screenshots/constitution-result.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
tests/screenshots/health-profile-add-allergy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
tests/screenshots/health-profile-add-medical.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
tests/screenshots/health-profile-cards.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
tests/screenshots/health-profile-edit-basic.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
tests/screenshots/health-profile-edit-lifestyle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/health-profile-final.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/health-profile-page.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
tests/screenshots/hp-allergy-add-after.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
tests/screenshots/hp-allergy-add-before.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
tests/screenshots/hp-allergy-saved.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
tests/screenshots/hp-basic-edit-after.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
tests/screenshots/hp-basic-edit-before.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
tests/screenshots/hp-basic-saved.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/hp-family-add-after.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
tests/screenshots/hp-family-add-before.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
tests/screenshots/hp-family-saved.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
tests/screenshots/hp-final-verification.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/hp-initial.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/hp-lifestyle-edit-after.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
tests/screenshots/hp-lifestyle-edit-before.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
tests/screenshots/hp-lifestyle-saved.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
tests/screenshots/hp-medical-add-after.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
tests/screenshots/hp-medical-add-before.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
tests/screenshots/hp-medical-delete-confirm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
tests/screenshots/hp-medical-delete-done.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
tests/screenshots/hp-medical-saved.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
tests/screenshots/profile-about-dialog.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
tests/screenshots/profile-edit-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
tests/screenshots/profile-health-profile.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
tests/screenshots/profile-logout-confirm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
tests/screenshots/profile-medication-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
tests/screenshots/profile-page.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Loading…
Cancel
Save