- constitution.test.js: 体质分析功能测试 - profile.test.js: "我的"页面功能测试 - health-profile-complete.test.js: 健康档案完整测试 - 包含测试使用文档 README.md Co-authored-by: Cursor <cursoragent@cursor.com>master
@ -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` - 存在测试失败 |
||||
@ -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(); |
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |