docs(e2e): 添加 E2E 测试设计规格文档
流程链路式双端 E2E 测试体系设计: - Web 端 5 条业务链路(Playwright + Page Object) - 小程序端 4 条业务链路(Vitest + miniprogram-automator) - API 驱动自建自毁数据策略,乐观锁 version 支持 - CI-ready 环境变量驱动设计
This commit is contained in:
603
docs/superpowers/specs/2026-04-28-e2e-testing-design.md
Normal file
603
docs/superpowers/specs/2026-04-28-e2e-testing-design.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# E2E 测试设计规格 — HMS 健康管理平台
|
||||
|
||||
> 日期: 2026-04-28 | 状态: 设计中
|
||||
|
||||
## Context
|
||||
|
||||
HMS 健康管理平台当前 E2E 测试仅有 10 个基础用例(登录/用户/插件/租户隔离),健康模块 ~27 个 Web 页面路由和微信小程序端完全无 E2E 覆盖。wiki 已将此标注为 P2 缺口。本设计旨在建立双端(Web + 小程序)的流程链路式 E2E 测试体系。
|
||||
|
||||
## 大纲
|
||||
|
||||
1. 整体架构与目录结构
|
||||
2. 关键业务链路定义
|
||||
3. 测试数据策略与 API Client
|
||||
4. Page Object 与 MCP Helper 设计
|
||||
5. 执行策略与 CI-Ready 设计
|
||||
|
||||
---
|
||||
|
||||
## §1 整体架构与目录结构
|
||||
|
||||
### 设计决策
|
||||
|
||||
| 维度 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| 覆盖范围 | Web 全量 + 小程序并行 | 双端同步覆盖,最大化业务保障 |
|
||||
| 测试深度 | 关键流程冒烟测试 | 务实覆盖最重要的用户路径 |
|
||||
| 小程序实现 | WeChat MCP 自动化 | 利用已有 MCP 联调能力 |
|
||||
| 测试架构 | 双端独立框架 | Web 用 Playwright,小程序用 Vitest + MCP |
|
||||
| 测试数据 | API 驱动自建自毁 | 每个测试独立创建/清理数据 |
|
||||
| CI 策略 | 本地优先,CI-ready 设计 | 环境变量驱动,未来可接入 GitHub Actions |
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
apps/web/e2e/ # Web 端 E2E(Playwright)
|
||||
├── flows/ # 业务链路测试
|
||||
│ ├── patient-journey.spec.ts # 患者全流程
|
||||
│ ├── vital-signs-flow.spec.ts # 体征数据链路
|
||||
│ ├── appointment-flow.spec.ts # 预约排班链路
|
||||
│ ├── follow-up-flow.spec.ts # 随访管理链路
|
||||
│ └── alert-flow.spec.ts # 告警处理链路
|
||||
├── smoke/ # 基础冒烟(迁移现有 4 个 spec)
|
||||
│ ├── login.spec.ts
|
||||
│ ├── users.spec.ts
|
||||
│ ├── plugins.spec.ts
|
||||
│ └── tenant-isolation.spec.ts
|
||||
├── fixtures/ # 测试基础设施
|
||||
│ ├── auth.fixture.ts # 认证 fixture(增强现有)
|
||||
│ ├── api-client.ts # API 数据准备/清理客户端
|
||||
│ └── test-data.ts # 测试数据工厂函数
|
||||
├── pages/ # 轻量 Page Object(仅关键页面)
|
||||
│ ├── login.page.ts
|
||||
│ ├── patient-list.page.ts
|
||||
│ ├── patient-detail.page.ts
|
||||
│ ├── appointment.page.ts
|
||||
│ └── health-data.page.ts
|
||||
└── playwright.config.ts # 增强配置
|
||||
|
||||
apps/miniprogram/e2e/ # 小程序端 E2E(MCP 自动化)
|
||||
├── flows/ # 业务链路测试
|
||||
│ ├── patient-health-view.spec.ts # 患者查看健康数据
|
||||
│ ├── vital-signs-input.spec.ts # 体征录入
|
||||
│ ├── points-flow.spec.ts # 积分签到兑换
|
||||
│ └── mall-flow.spec.ts # 积分商城
|
||||
├── helpers/ # MCP 封装
|
||||
│ ├── mcp-client.ts # MCP 工具调用封装
|
||||
│ ├── auth.helper.ts # 小程序登录/Token 注入
|
||||
│ └── navigation.helper.ts # 页面导航封装
|
||||
├── check-readiness.ts # 环境就绪检查
|
||||
└── vitest.config.ts # 测试运行器配置
|
||||
```
|
||||
|
||||
### 关键原则
|
||||
|
||||
1. 现有 4 个 smoke test 原地迁移到 `smoke/` 目录,逻辑不变
|
||||
2. Web 端保持 Playwright,小程序端用 Vitest + `miniprogram-automator`,各自独立运行
|
||||
3. 两端 API client 逻辑保持一致但代码各自维护(Web 运行在 Node,小程序也运行在 Node,可考虑后续抽取共享包)
|
||||
4. Page Object 只封装关键页面,随访/告警等简单页面直接在 spec 中操作
|
||||
5. 每条链路自包含(API 准备所需数据),不依赖其他链路的执行顺序
|
||||
6. 所有 Page Object 使用 Hash Router 路径格式(`/#/health/patients`)
|
||||
|
||||
## §2 关键业务链路定义
|
||||
|
||||
### Web 端链路(5 条)
|
||||
|
||||
**Flow 1: 患者全流程 `patient-journey.spec.ts`**
|
||||
```
|
||||
登录 → 患者列表页 → 创建患者 → 查看患者详情 → 编辑患者信息
|
||||
→ 分配医生 → 添加家族成员 → 删除测试数据
|
||||
```
|
||||
验证点:患者 CRUD、详情页数据完整性、医生关联、家族成员管理
|
||||
|
||||
**Flow 2: 体征数据链路 `vital-signs-flow.spec.ts`**
|
||||
```
|
||||
登录 → 选择患者 → 录入体征数据(血压/心率/血糖)
|
||||
→ 查看体征列表 → 查看趋势图 → 录入化验报告 → 审核报告
|
||||
→ 清理数据
|
||||
```
|
||||
验证点:体征录入表单、列表展示、趋势图表渲染、化验报告上传与审核
|
||||
|
||||
**Flow 3: 预约排班链路 `appointment-flow.spec.ts`**
|
||||
```
|
||||
登录 → 创建医生 → 设置排班 → 患者列表 → 创建预约
|
||||
→ 查看预约列表 → 更新预约状态 → 清理数据
|
||||
```
|
||||
验证点:排班日历交互、预约创建、状态流转(待确认→已确认→已完成)
|
||||
|
||||
**Flow 4: 随访管理链路 `follow-up-flow.spec.ts`**
|
||||
```
|
||||
登录 → 创建随访模板 → 批量创建随访任务 → 查看任务列表
|
||||
→ 完成随访(填写记录)→ 查看随访记录 → 清理数据
|
||||
```
|
||||
验证点:模板管理、批量操作、任务分配、随访记录填写
|
||||
|
||||
**Flow 5: 告警处理链路 `alert-flow.spec.ts`**
|
||||
```
|
||||
登录(API) → 创建告警规则(低阈值,如心率 > 50)
|
||||
→ 录入触发体征数据(API 创建心率为 110 的体征)→ 轮询等待告警生成(最多 10s)
|
||||
→ 查看告警列表(UI)→ 确认告警 → 处理告警 → 查看告警规则
|
||||
→ 停用规则 → 清理数据(规则 + 体征 + 告警)
|
||||
```
|
||||
验证点:告警规则配置、告警自动生成、列表/仪表盘展示、状态流转(待处理→已确认→已处理)
|
||||
**异步处理说明:** 告警由后台任务根据规则匹配体征数据自动生成。测试通过 API 创建低阈值规则 + 触发数据,然后轮询告警列表 API 等待告警出现(`await expect.poll(() => api.getAlerts(), { timeout: 10_000 })`)。
|
||||
|
||||
### 小程序端链路(4 条)
|
||||
|
||||
**Flow 6: 患者健康数据查看 `patient-health-view.spec.ts`**
|
||||
```
|
||||
MCP 登录 → 首页 → 查看今日体征摘要 → 查看体征趋势
|
||||
→ 查看随访任务列表 → 查看化验报告
|
||||
```
|
||||
|
||||
**Flow 7: 体征数据录入 `vital-signs-input.spec.ts`**
|
||||
```
|
||||
MCP 登录 → 进入录入页 → 填写血压/心率 → 提交
|
||||
→ 验证数据出现在列表 → 清理数据
|
||||
```
|
||||
|
||||
**Flow 8: 积分签到兑换 `points-flow.spec.ts`**
|
||||
```
|
||||
MCP 登录 → 每日签到 → 查看积分余额 → 浏览商品
|
||||
→ 兑换商品 → 查看订单列表
|
||||
```
|
||||
|
||||
**Flow 9: 积分商城 `mall-flow.spec.ts`**
|
||||
```
|
||||
MCP 登录 → 商城首页 → 浏览商品分类 → 查看商品详情
|
||||
→ 查看线下活动 → 活动报名
|
||||
```
|
||||
|
||||
### 链路覆盖矩阵
|
||||
|
||||
| 业务模块 | Web 链路 | 小程序链路 | API 链路(已有) |
|
||||
|---------|---------|-----------|---------------|
|
||||
| 患者管理 | Flow 1 | — | Chain 1 |
|
||||
| 体征/健康数据 | Flow 2 | Flow 6, 7 | Chain 1, 2 |
|
||||
| 预约排班 | Flow 3 | — | Chain 1 |
|
||||
| 随访管理 | Flow 4 | Flow 6 | Chain 1 |
|
||||
| 告警系统 | Flow 5 | — | — |
|
||||
| 积分体系 | — | Flow 8 | Chain 4 |
|
||||
| 积分商城 | — | Flow 9 | Chain 4 |
|
||||
|
||||
未覆盖模块说明:
|
||||
- **咨询管理**(2 页面)— 有完整 API 链路,但实时消息交互难以用 E2E 验证,建议 API 集成测试覆盖
|
||||
- **内容/文章管理**(4 页面)— CRUD 操作较标准,优先级低于核心医疗流程
|
||||
- **AI 分析**(3 页面)— 依赖外部 AI 服务,不稳定,建议 mock 测试
|
||||
- **统计仪表盘**(5 页面)— 数据聚合展示,E2E 价值有限,建议视觉回归测试
|
||||
|
||||
## §3 测试数据策略与 API Client
|
||||
|
||||
### 核心原则
|
||||
|
||||
API 驱动自建自毁:每个测试通过 API 创建所需数据,测试结束后清理。不依赖数据库预置状态。
|
||||
|
||||
### API Client
|
||||
|
||||
后端所有实体使用乐观锁(`version` 字段),更新/删除操作必须携带 version。API Client 的所有方法返回完整实体(含 version),清理操作使用返回的 version。
|
||||
|
||||
```typescript
|
||||
// fixtures/api-client.ts
|
||||
|
||||
interface VersionedEntity { id: string; version: number }
|
||||
|
||||
class ApiClient {
|
||||
private token: string;
|
||||
private baseUrl: string;
|
||||
|
||||
// 认证
|
||||
async login(username: string, password: string): Promise<AuthResponse>
|
||||
async loginAsAdmin(): Promise<AuthResponse>
|
||||
|
||||
// 患者数据工厂 — 返回含 version 的完整实体
|
||||
async createPatient(overrides?: Partial<PatientData>): Promise<Patient & VersionedEntity>
|
||||
async updatePatient(id: string, version: number, data: Partial<PatientData>): Promise<Patient & VersionedEntity>
|
||||
async deletePatient(id: string, version: number): Promise<void>
|
||||
|
||||
// 医生数据工厂
|
||||
async createDoctor(overrides?: Partial<DoctorData>): Promise<Doctor & VersionedEntity>
|
||||
async deleteDoctor(id: string, version: number): Promise<void>
|
||||
|
||||
// 体征数据工厂
|
||||
async createVitalSigns(patientId: string, overrides?: Partial<VitalSignsData>): Promise<VitalSigns & VersionedEntity>
|
||||
async deleteVitalSigns(patientId: string, id: string, version: number): Promise<void>
|
||||
|
||||
// 预约数据工厂
|
||||
async createAppointment(overrides?: Partial<AppointmentData>): Promise<Appointment & VersionedEntity>
|
||||
async updateAppointmentStatus(id: string, version: number, status: string): Promise<Appointment & VersionedEntity>
|
||||
async deleteAppointment(id: string, version: number): Promise<void>
|
||||
|
||||
// 随访数据工厂
|
||||
async createFollowUpTemplate(overrides?: Partial<TemplateData>): Promise<Template & VersionedEntity>
|
||||
async createFollowUpTask(overrides?: Partial<TaskData>): Promise<Task & VersionedEntity>
|
||||
async deleteFollowUpTemplate(id: string, version: number): Promise<void>
|
||||
async deleteFollowUpTask(id: string, version: number): Promise<void>
|
||||
|
||||
// 告警数据工厂
|
||||
async createAlertRule(overrides?: Partial<AlertRuleData>): Promise<AlertRule & VersionedEntity>
|
||||
async deleteAlertRule(id: string, version: number): Promise<void>
|
||||
// 告警状态流转(需要 version)
|
||||
async acknowledgeAlert(id: string, version: number): Promise<Alert & VersionedEntity>
|
||||
async resolveAlert(id: string, version: number): Promise<Alert & VersionedEntity>
|
||||
async dismissAlert(id: string, version: number): Promise<Alert & VersionedEntity>
|
||||
|
||||
// 排班数据工厂
|
||||
async createSchedule(overrides?: Partial<ScheduleData>): Promise<Schedule & VersionedEntity>
|
||||
async deleteSchedule(id: string, version: number): Promise<void>
|
||||
|
||||
// 通用 HTTP
|
||||
async get<T>(path: string): Promise<T>
|
||||
async post<T>(path: string, body: unknown): Promise<T>
|
||||
async put<T>(path: string, body: unknown): Promise<T>
|
||||
async delete(path: string, body?: unknown): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### 测试数据工厂
|
||||
|
||||
```typescript
|
||||
// fixtures/test-data.ts
|
||||
|
||||
function makePatient(overrides?: Partial<PatientData>): PatientData {
|
||||
const id = `test_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
return {
|
||||
name: `测试患者_${id}`,
|
||||
gender: 'male',
|
||||
birthDate: '1990-01-15',
|
||||
phone: `138${Math.random().toString().slice(2, 11)}`,
|
||||
idNumber: `110101199001${Math.random().toString().slice(2, 8)}`,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeDoctor(overrides?: Partial<DoctorData>): DoctorData { ... }
|
||||
function makeVitalSigns(overrides?: Partial<VitalSignsData>): VitalSignsData { ... }
|
||||
function makeAppointment(patientId: string, doctorId: string, scheduleId: string): AppointmentData { ... }
|
||||
```
|
||||
|
||||
每次生成唯一数据(时间戳 + 随机串),避免测试间冲突。
|
||||
|
||||
### 数据生命周期管理
|
||||
|
||||
所有删除/更新操作必须携带 version(乐观锁)。创建操作的返回值包含 version,清理时使用。
|
||||
|
||||
**Web 端(Playwright)— try/finally 模式:**
|
||||
```typescript
|
||||
test('患者全流程', async ({ page, api }) => {
|
||||
const cleanup: Array<() => Promise<void>> = [];
|
||||
try {
|
||||
const patient = await api.createPatient();
|
||||
// 创建时注册清理函数,保存 id + version
|
||||
cleanup.push(() => api.deletePatient(patient.id, patient.version));
|
||||
|
||||
const doctor = await api.createDoctor();
|
||||
cleanup.push(() => api.deleteDoctor(doctor.id, doctor.version));
|
||||
|
||||
// ... 测试操作(更新时使用最新 version)...
|
||||
} finally {
|
||||
// 逆序清理(后创建的先删除,避免外键约束问题)
|
||||
for (const fn of cleanup.reverse()) {
|
||||
await fn().catch(() => {}); // 忽略已删除的错误
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**小程序端(Vitest)— afterEach 模式:**
|
||||
```typescript
|
||||
describe('体征数据录入', () => {
|
||||
const cleanup: Array<() => Promise<void>> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const fn of cleanup.reverse()) await fn().catch(() => {});
|
||||
cleanup.length = 0;
|
||||
});
|
||||
|
||||
it('录入血压数据', async () => {
|
||||
const vitalSigns = await api.createVitalSigns(patientId);
|
||||
cleanup.push(() => api.deleteVitalSigns(patientId, vitalSigns.id, vitalSigns.version));
|
||||
// ... 断言 ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Fixture 组合(Web Playwright):** 将认证 page 和 ApiClient 注入到同一个 test fixture:
|
||||
```typescript
|
||||
// fixtures/auth.fixture.ts — 增强版
|
||||
import { test as base } from '@playwright/test';
|
||||
import { ApiClient } from './api-client';
|
||||
|
||||
type E2eFixtures = {
|
||||
api: ApiClient;
|
||||
authenticatedPage: Page;
|
||||
};
|
||||
|
||||
export const test = base.extend<E2eFixtures>({
|
||||
// 注入已认证的 ApiClient
|
||||
api: async ({}, use) => {
|
||||
const client = new ApiClient();
|
||||
await client.loginAsAdmin();
|
||||
await use(client);
|
||||
},
|
||||
// 注入已认证的 page(继承现有 auth.fixture 逻辑)
|
||||
authenticatedPage: async ({ page }, use) => {
|
||||
const client = new ApiClient();
|
||||
const { access_token, refresh_token, user } = await client.loginAsAdmin();
|
||||
await page.addInitScript(({ token, refresh, userData }) => {
|
||||
localStorage.setItem('access_token', token);
|
||||
localStorage.setItem('refresh_token', refresh);
|
||||
localStorage.setItem('user', userData);
|
||||
}, { token: access_token, refresh: refresh_token, userData: JSON.stringify(user) });
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 认证策略
|
||||
|
||||
| 端 | 认证方式 | 账号 |
|
||||
|---|---------|------|
|
||||
| Web(admin) | API login → localStorage 注入 | `admin` / `Admin@2026` |
|
||||
| Web(受限角色) | 同上,不同账号 | 用于权限验证场景 |
|
||||
| 小程序 | MCP ensureConnection → storage 注入 token | `mp_e2e_test` / `Test@2026` |
|
||||
|
||||
凭证从环境变量读取,fallback 到开发默认值:
|
||||
```typescript
|
||||
const TEST_ADMIN = {
|
||||
username: process.env.E2E_ADMIN_USER || 'admin',
|
||||
password: process.env.E2E_ADMIN_PASS || 'Admin@2026',
|
||||
};
|
||||
```
|
||||
|
||||
## §4 Page Object 与 MCP Helper 设计
|
||||
|
||||
### Web 端 Page Object(5 个,轻量级)
|
||||
|
||||
每个 Page Object 包含 3 部分:**导航**、**操作**、**断言辅助**。只封装关键页面,随访/告警等简单页面直接在 spec 中操作。
|
||||
|
||||
**Login.page.ts:**
|
||||
```typescript
|
||||
class LoginPage {
|
||||
async goto(): Promise<void>
|
||||
async login(username: string, password: string): Promise<void>
|
||||
async getErrorMessage(): Promise<string>
|
||||
async isLoggedIn(): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
**PatientList.page.ts:**
|
||||
```typescript
|
||||
class PatientListPage {
|
||||
async goto(): Promise<void> // 导航 + 等待表格加载
|
||||
async clickCreate(): Promise<void>
|
||||
async fillCreateForm(data: PatientData): Promise<void>
|
||||
async submitCreateForm(): Promise<void>
|
||||
async searchPatient(name: string): Promise<void>
|
||||
async clickPatientRow(index: number): Promise<void>
|
||||
async getTableRowCount(): Promise<number>
|
||||
async hasPatientInTable(name: string): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
**PatientDetail.page.ts:**
|
||||
```typescript
|
||||
class PatientDetailPage {
|
||||
async goto(id: string): Promise<void>
|
||||
async getPatientName(): Promise<string>
|
||||
async clickTab(tabName: string): Promise<void> // 体征/随访/化验/诊断
|
||||
async getVitalSignsCount(): Promise<number>
|
||||
async assignDoctor(doctorName: string): Promise<void>
|
||||
async addFamilyMember(data: FamilyMemberData): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
**HealthData.page.ts:**
|
||||
```typescript
|
||||
class HealthDataPage {
|
||||
async clickAddVitalSigns(): Promise<void>
|
||||
async fillVitalSignsForm(data: VitalSignsData): Promise<void>
|
||||
async submitVitalSigns(): Promise<void>
|
||||
async getVitalSignsList(): Promise<VitalSignsData[]>
|
||||
async trendChartIsVisible(): Promise<boolean>
|
||||
async uploadLabReport(filePath: string): Promise<void>
|
||||
async reviewLabReport(reportId: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
**Appointment.page.ts:**
|
||||
```typescript
|
||||
class AppointmentPage {
|
||||
async gotoSchedule(): Promise<void>
|
||||
async gotoAppointments(): Promise<void>
|
||||
async selectDateOnCalendar(date: string): Promise<void>
|
||||
async createScheduleSlot(data: ScheduleData): Promise<void>
|
||||
async createAppointment(data: AppointmentData): Promise<void>
|
||||
async updateStatus(appointmentId: string, status: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### 小程序端 Automator Helper(3 个)
|
||||
|
||||
小程序使用 `miniprogram-automator` npm 包(项目已有依赖,见 `apps/miniprogram/test-automator.mjs`),
|
||||
通过 WeChat DevTools 的自动化接口操作小程序。与 MCP 工具相同的底层能力,但可独立运行在 Vitest 中。
|
||||
|
||||
**前置条件:** 需要先构建小程序(`pnpm build:weapp`),WeChat DevTools 需打开项目并启用自动化端口。
|
||||
|
||||
**AutomatorClient:** 封装 miniprogram-automator 为 TypeScript API
|
||||
```typescript
|
||||
import automator from 'miniprogram-automator';
|
||||
|
||||
class AutomatorClient {
|
||||
private mini: automator.MiniProgram;
|
||||
|
||||
// 连接/启动小程序
|
||||
async connect(cliPath?: string, projectPath?: string): Promise<void>
|
||||
async disconnect(): Promise<void>
|
||||
|
||||
// 页面操作
|
||||
async currentPage(): Promise<automator.Page>
|
||||
async navigateTo(path: string, query?: Record<string, string>): Promise<void>
|
||||
async navigateBack(): Promise<void>
|
||||
async reLaunch(path: string): Promise<void>
|
||||
|
||||
// 元素操作
|
||||
async waitForElement(selector: string, timeout?: number): Promise<automator.Element>
|
||||
async tap(selector: string): Promise<void>
|
||||
async inputText(selector: string, value: string): Promise<void>
|
||||
async getElement(selector: string): Promise<automator.Element>
|
||||
async getElements(selector: string): Promise<automator.Element[]>
|
||||
|
||||
// 页面数据
|
||||
async getPageData(path?: string): Promise<any>
|
||||
async screenshot(path?: string): Promise<Buffer>
|
||||
}
|
||||
```
|
||||
|
||||
**MpAuthHelper:** 小程序认证
|
||||
```typescript
|
||||
class MpAuthHelper {
|
||||
constructor(private client: AutomatorClient, private api: ApiClient) {}
|
||||
// API 获取 token → 通过 automator 写入 storage → reLaunch 刷新
|
||||
async loginAsTestPatient(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
**MpNavigator:** 小程序页面导航封装
|
||||
```typescript
|
||||
class MpNavigator {
|
||||
constructor(private client: AutomatorClient) {}
|
||||
async goToHealthHome(): Promise<void> // reLaunch /pages/pkg-health/index
|
||||
async goToVitalSignsInput(): Promise<void> // navigateTo 输入页
|
||||
async goToVitalSignsTrend(): Promise<void> // navigateTo 趋势页
|
||||
async goToPointsCheckin(): Promise<void> // navigateTo 签到页
|
||||
async goToMall(): Promise<void> // reLaunch 商城首页
|
||||
async goToFollowUpTasks(): Promise<void> // navigateTo 随访列表
|
||||
async goToOrders(): Promise<void> // navigateTo 订单列表
|
||||
}
|
||||
```
|
||||
|
||||
**小程序页面路径参考(来自 app.config.ts):**
|
||||
- 患者首页: `/pages/pkg-health/index`
|
||||
- 体征录入: `/pages/pkg-health/input/index`
|
||||
- 体征趋势: `/pages/pkg-health/trend/index`
|
||||
- 积分签到: `/pages/pkg-profile/index`(签到入口在个人中心)
|
||||
- 积分商城: `/pages/pkg-mall/index`
|
||||
- 随访任务: `/pages/pkg-health/followups/index`
|
||||
- 订单列表: `/pages/pkg-mall/orders/index`
|
||||
|
||||
### 层级关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Flow Spec │ ← 业务链路测试用例
|
||||
├──────────────┬──────────────────────────┤
|
||||
│ Page Object │ MCP Helper │ ← UI 操作抽象
|
||||
│ (Web 端) │ (小程序端) │
|
||||
├──────────────┴──────────────────────────┤
|
||||
│ API Client │ ← 数据准备/清理
|
||||
├─────────────────────────────────────────┤
|
||||
│ Test Data Factory │ ← 随机数据生成
|
||||
├─────────────────────────────────────────┤
|
||||
│ Auth Fixture / Auth Helper │ ← 认证
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## §5 执行策略与 CI-Ready 设计
|
||||
|
||||
### 本地执行命令
|
||||
|
||||
```bash
|
||||
# Web 端 E2E
|
||||
cd apps/web
|
||||
pnpm test:e2e # 运行全部(smoke + flows)
|
||||
pnpm test:e2e -- --grep @smoke # 只跑冒烟
|
||||
pnpm test:e2e -- --grep "患者全流程" # 指定链路
|
||||
pnpm test:e2e:ui # 交互式 UI 模式
|
||||
|
||||
# 小程序端 E2E
|
||||
cd apps/miniprogram
|
||||
pnpm test:e2e # 运行全部
|
||||
pnpm test:e2e -- --reporter=verbose # 详细输出
|
||||
```
|
||||
|
||||
### Playwright Config 增强
|
||||
|
||||
关键变更:
|
||||
- `testDir: './e2e'`(保持不变,smoke/ 和 flows/ 子目录都在 e2e 下)
|
||||
- `testMatch: ['smoke/**/*.spec.ts', 'flows/**/*.spec.ts']`
|
||||
- `timeout: 60_000`(链路测试需要更长时间)
|
||||
- `fullyParallel: false`(链路间有数据依赖,串行更稳定)
|
||||
- `reporter: [['html'], ['list']]`
|
||||
- `globalSetup: './e2e/check-readiness'`
|
||||
- 标签过滤:`@smoke` 快速验证,`@flow` 完整链路
|
||||
|
||||
### 小程序 Vitest Config
|
||||
|
||||
**新增依赖:** `apps/miniprogram/package.json` 需添加:
|
||||
- `vitest`(测试运行器)
|
||||
- `test:e2e` script
|
||||
- `miniprogram-automator` 已在项目中(`test-automator.mjs` 使用)
|
||||
|
||||
配置:
|
||||
- `testTimeout: 30_000`
|
||||
- `testSequence: 'sequential'`(automator 操作必须串行)
|
||||
- `globalSetup: check-readiness`
|
||||
- 测试前需执行 `pnpm build:weapp` 构建小程序
|
||||
- WeChat DevTools 必须打开项目并启用自动化端口
|
||||
|
||||
### 前置条件检查
|
||||
|
||||
测试运行前自动验证环境就绪。Web 端检查 `http://localhost:3000/health/live` + `http://localhost:5174`。小程序端额外检查 WeChat DevTools 自动化端口可达。最多重试 5 次,间隔 2s,任一失败则打印明确提示并退出。
|
||||
|
||||
### CI-Ready 设计
|
||||
|
||||
本地优先,但设计上保证 CI 兼容:
|
||||
|
||||
**环境变量驱动:**
|
||||
```bash
|
||||
E2E_BASE_URL=http://localhost:5174
|
||||
E2E_API_URL=http://localhost:3000
|
||||
E2E_ADMIN_USER=admin
|
||||
E2E_ADMIN_PASS=Admin@2026
|
||||
E2E_MP_USER=mp_e2e_test
|
||||
E2E_MP_PASS=Test@2026
|
||||
E2E_HEADLESS=true
|
||||
E2E_SKIP_MP=false # CI 中可跳过小程序测试
|
||||
```
|
||||
|
||||
**GitHub Actions 预留:**
|
||||
```yaml
|
||||
# .github/workflows/e2e.yml — 未来启用
|
||||
name: E2E Tests
|
||||
jobs:
|
||||
web-e2e:
|
||||
services:
|
||||
postgres: { ... }
|
||||
steps:
|
||||
- run: cargo run -p erp-server &
|
||||
- run: cd apps/web && pnpm test:e2e
|
||||
```
|
||||
|
||||
**测试结果输出:**
|
||||
- Playwright HTML 报告 → `apps/web/e2e-results/`
|
||||
- Vitest JUnit 报告 → `apps/miniprogram/e2e-results/`
|
||||
|
||||
### 执行耗时预估
|
||||
|
||||
| 场景 | 命令 | 预计耗时 |
|
||||
|-----|------|---------|
|
||||
| Web 全量 | `pnpm test:e2e` | ~3-5 分钟 |
|
||||
| Web 冒烟 | `--grep @smoke` | ~30 秒 |
|
||||
| Web 单条链路 | `--grep "患者"` | ~30-60 秒 |
|
||||
| 小程序全量 | `pnpm test:e2e` | ~2-4 分钟 |
|
||||
| 双端全量 | 根目录脚本串联 | ~6-10 分钟 |
|
||||
|
||||
---
|
||||
|
||||
## 验证方式
|
||||
|
||||
1. `cd apps/web && pnpm test:e2e` — Web 端全部通过
|
||||
2. `cd apps/miniprogram && pnpm test:e2e` — 小程序端全部通过
|
||||
3. 确认测试数据已清理(数据库无残留测试记录)
|
||||
4. `--grep @smoke` 可在 30 秒内完成冒烟验证
|
||||
5. HTML 报告可正常打开查看
|
||||
Reference in New Issue
Block a user