test(web+mp): E2E 测试全量实施 — Web 5 flow + MP 4 flow + 基础设施
Web 端 (Playwright): - fixtures: test-data 工厂 + API Client (乐观锁 version) + 增强 auth fixture - pages: LoginPage, PatientListPage, PatientDetailPage, HealthDataPage, AppointmentPage - flows: 患者全流程, 体征数据链路, 预约排班链路, 随访管理链路, 告警处理链路 - smoke tests 迁移到 smoke/ 目录,import 路径更新 - playwright.config.ts 更新: globalSetup 环境检查, 60s timeout, video retain 小程序端 (Vitest + miniprogram-automator): - helpers: AutomatorClient, MpApiClient, MpAuthHelper, MpNavigator - flows: 患者健康数据查看, 体征数据录入, 积分签到兑换, 积分商城浏览 - vitest.config.ts + check-readiness.ts - vitest 4.1.5 依赖安装 Playwright 发现 15 个测试 (5 flow + 10 smoke),全部就绪
This commit is contained in:
70
apps/miniprogram/e2e/helpers/api-client.ts
Normal file
70
apps/miniprogram/e2e/helpers/api-client.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// apps/miniprogram/e2e/helpers/api-client.ts
|
||||
// 简化版 API Client,用于小程序 E2E 数据准备/清理
|
||||
|
||||
const API_BASE = process.env.E2E_API_URL || 'http://localhost:3000/api/v1';
|
||||
|
||||
export class MpApiClient {
|
||||
private token = '';
|
||||
|
||||
async login(username?: string, password?: string) {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: username || process.env.E2E_ADMIN_USER || 'admin',
|
||||
password: password || process.env.E2E_ADMIN_PASS || 'Admin@2026',
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) throw new Error('Login failed');
|
||||
this.token = json.data.access_token;
|
||||
return json.data;
|
||||
}
|
||||
|
||||
getToken() { return this.token; }
|
||||
|
||||
async createPatient(overrides?: Record<string, unknown>) {
|
||||
return this.post('/health/patients', overrides ?? {});
|
||||
}
|
||||
|
||||
async deletePatient(id: string, version: number) {
|
||||
await this.del(`/health/patients/${id}`, { version });
|
||||
}
|
||||
|
||||
async createVitalSigns(patientId: string, overrides?: Record<string, unknown>) {
|
||||
return this.post(`/health/patients/${patientId}/vital-signs`, overrides ?? {});
|
||||
}
|
||||
|
||||
async deleteVitalSigns(patientId: string, id: string, version: number) {
|
||||
await this.del(`/health/patients/${patientId}/vital-signs/${id}`, { version });
|
||||
}
|
||||
|
||||
async listPointsProducts() {
|
||||
return this.get('/health/points/products');
|
||||
}
|
||||
|
||||
private async headers() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
}
|
||||
|
||||
private async get(path: string) {
|
||||
const res = await fetch(`${API_BASE}${path}`, { headers: await this.headers() });
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
}
|
||||
|
||||
private async post(path: string, body: unknown) {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: 'POST', headers: await this.headers(), body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) throw new Error(`POST ${path} failed`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
private async del(path: string, body?: unknown) {
|
||||
await fetch(`${API_BASE}${path}`, {
|
||||
method: 'DELETE', headers: await this.headers(), body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
26
apps/miniprogram/e2e/helpers/auth.helper.ts
Normal file
26
apps/miniprogram/e2e/helpers/auth.helper.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// apps/miniprogram/e2e/helpers/auth.helper.ts
|
||||
import { AutomatorClient } from './automator-client';
|
||||
import { MpApiClient } from './api-client';
|
||||
|
||||
export class MpAuthHelper {
|
||||
constructor(
|
||||
private client: AutomatorClient,
|
||||
private api: MpApiClient,
|
||||
) {}
|
||||
|
||||
async loginAsTestPatient() {
|
||||
const loginRes = await this.api.login(
|
||||
process.env.E2E_MP_USER || 'mp_e2e_test',
|
||||
process.env.E2E_MP_PASS || 'Test@2026',
|
||||
);
|
||||
|
||||
await this.client.reLaunch('/pages/index/index');
|
||||
const page = await this.client.currentPage();
|
||||
|
||||
await this.client.callMethod('page', 'setData', {
|
||||
'access_token': loginRes.access_token,
|
||||
});
|
||||
|
||||
await this.client.reLaunch('pages/index/index');
|
||||
}
|
||||
}
|
||||
96
apps/miniprogram/e2e/helpers/automator-client.ts
Normal file
96
apps/miniprogram/e2e/helpers/automator-client.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// apps/miniprogram/e2e/helpers/automator-client.ts
|
||||
import automator from 'miniprogram-automator';
|
||||
|
||||
const DEFAULT_CLI_PATH = 'C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat';
|
||||
const DEFAULT_PROJECT_PATH = process.cwd();
|
||||
|
||||
export class AutomatorClient {
|
||||
private mini: automator.MiniProgram | null = null;
|
||||
|
||||
async connect(cliPath?: string, projectPath?: string) {
|
||||
this.mini = await automator.launch({
|
||||
cliPath: cliPath || DEFAULT_CLI_PATH,
|
||||
projectPath: projectPath || DEFAULT_PROJECT_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.mini) {
|
||||
await this.mini.close();
|
||||
this.mini = null;
|
||||
}
|
||||
}
|
||||
|
||||
private getMini(): automator.MiniProgram {
|
||||
if (!this.mini) throw new Error('AutomatorClient 未连接,请先调用 connect()');
|
||||
return this.mini;
|
||||
}
|
||||
|
||||
async currentPage(): Promise<automator.Page> {
|
||||
return this.getMini().currentPage();
|
||||
}
|
||||
|
||||
async navigateTo(path: string, _query?: Record<string, string>) {
|
||||
const page = await this.getMini().navigateTo(`/${path.replace(/^\//, '')}`);
|
||||
return page;
|
||||
}
|
||||
|
||||
async navigateBack() {
|
||||
await this.getMini().navigateBack();
|
||||
}
|
||||
|
||||
async reLaunch(path: string) {
|
||||
await this.getMini().reLaunch(`/${path.replace(/^\//, '')}`);
|
||||
}
|
||||
|
||||
async tap(selector: string) {
|
||||
const page = this.getMini().currentPage();
|
||||
const element = await page.$(selector);
|
||||
if (!element) throw new Error(`元素未找到: ${selector}`);
|
||||
await element.tap();
|
||||
}
|
||||
|
||||
async inputText(selector: string, value: string) {
|
||||
const page = this.getMini().currentPage();
|
||||
const element = await page.$(selector);
|
||||
if (!element) throw new Error(`元素未找到: ${selector}`);
|
||||
await element.setValue(value);
|
||||
}
|
||||
|
||||
async getElement(selector: string) {
|
||||
const page = this.getMini().currentPage();
|
||||
return page.$(selector);
|
||||
}
|
||||
|
||||
async getElements(selector: string) {
|
||||
const page = this.getMini().currentPage();
|
||||
return page.$$(selector);
|
||||
}
|
||||
|
||||
async waitForElement(selector: string, timeout = 5000): Promise<automator.Element> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
const el = await this.getElement(selector);
|
||||
if (el) return el;
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
throw new Error(`等待元素超时: ${selector} (${timeout}ms)`);
|
||||
}
|
||||
|
||||
async getPageData(path?: string) {
|
||||
const page = this.getMini().currentPage();
|
||||
return page.data(path);
|
||||
}
|
||||
|
||||
async screenshot(path?: string): Promise<Buffer> {
|
||||
const page = this.getMini().currentPage();
|
||||
return page.screenshot({ path });
|
||||
}
|
||||
|
||||
async callMethod(selector: string, method: string, ...args: unknown[]) {
|
||||
const page = this.getMini().currentPage();
|
||||
const element = await page.$(selector);
|
||||
if (!element) throw new Error(`元素未找到: ${selector}`);
|
||||
return element.callMethod(method, ...args);
|
||||
}
|
||||
}
|
||||
34
apps/miniprogram/e2e/helpers/navigation.helper.ts
Normal file
34
apps/miniprogram/e2e/helpers/navigation.helper.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// apps/miniprogram/e2e/helpers/navigation.helper.ts
|
||||
import { AutomatorClient } from './automator-client';
|
||||
|
||||
export class MpNavigator {
|
||||
constructor(private client: AutomatorClient) {}
|
||||
|
||||
async goToHealthHome() {
|
||||
await this.client.reLaunch('pages/pkg-health/index');
|
||||
}
|
||||
|
||||
async goToVitalSignsInput() {
|
||||
await this.client.navigateTo('pages/pkg-health/input/index');
|
||||
}
|
||||
|
||||
async goToVitalSignsTrend() {
|
||||
await this.client.navigateTo('pages/pkg-health/trend/index');
|
||||
}
|
||||
|
||||
async goToProfile() {
|
||||
await this.client.navigateTo('pages/pkg-profile/index');
|
||||
}
|
||||
|
||||
async goToMall() {
|
||||
await this.client.reLaunch('pages/pkg-mall/index');
|
||||
}
|
||||
|
||||
async goToFollowUpTasks() {
|
||||
await this.client.navigateTo('pages/pkg-health/followups/index');
|
||||
}
|
||||
|
||||
async goToOrders() {
|
||||
await this.client.navigateTo('pages/pkg-mall/orders/index');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user