Files
hms/apps/miniprogram/e2e-final.cjs
iven d460316d23
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
test(miniprogram): 端到端链路验证脚本 — 11 UI链路 + 10 API闭环
- 连接微信开发者工具 automator (ws://localhost:9420)
- 通过加密 storage 注入 admin token 绕过微信登录
- 验证 11 条 UI 链路: 首页/健康数据/录入/日常监测/积分商城/
  预约/家庭成员/咨询/文章/趋势/报告
- 验证 10 个 API 数据闭环: 患者/预约/咨询/日常监测/积分/
  签到/商品/医生/文章/随访
- 正确处理 tabbar 页面 (switchTab vs navigateTo)
- 所有导航带 8s 超时保护
2026-04-27 08:20:26 +08:00

420 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* HMS 小程序端到端链路验证 v4 (final)
* 前置条件: dist/ 已构建, 开发者工具已打开项目, 自动化端口 9420 已开放
*/
const automator = require('miniprogram-automator');
const http = require('http');
const CryptoJS = require('crypto-js');
const ENC_KEY = '0a17b71d46064b06f993c9c202b342425e311a79f5be026d830562e7ad51f522';
function encrypt(plaintext) { return CryptoJS.AES.encrypt(plaintext, ENC_KEY).toString(); }
const BASE = 'http://localhost:3000/api/v1';
const results = [];
function log(chain, step, status, detail) {
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⚠️';
console.log(` ${icon} [${chain}] ${step}: ${detail}`);
results.push({ chain, step, status, detail });
}
function api(method, path, body, token) {
return new Promise((resolve, reject) => {
const url = new URL(BASE + path);
const opts = {
hostname: url.hostname, port: url.port,
path: url.pathname + url.search, method,
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
};
if (token) opts.headers['Authorization'] = `Bearer ${token}`;
const req = http.request(opts, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(Buffer.concat(chunks).toString()) }); }
catch { resolve({ status: res.statusCode, raw: true }); }
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
const T = (ms) => new Promise((_, r) => setTimeout(() => r(new Error('timeout')), ms));
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function main() {
console.log('\n=== HMS 小程序端到端链路验证 ===\n');
// ====== 连接 ======
console.log('连接微信开发者工具...');
const mini = await automator.connect({ wsEndpoint: 'ws://localhost:9420' });
const info = await mini.systemInfo();
console.log(`已连接 (SDK ${info.SDKVersion}, ${info.model})\n`);
// ====== 辅助 ======
async function curPage() {
const page = await mini.currentPage();
return page;
}
async function curPath() {
const page = await curPage();
return page.path;
}
const tabPages = new Set(['pages/index/index', 'pages/health/index', 'pages/consultation/index', 'pages/mall/index', 'pages/profile/index']);
async function nav(url) {
const cleanUrl = url.startsWith('/') ? url.slice(1) : url;
try {
if (tabPages.has(cleanUrl)) {
await Promise.race([mini.switchTab('/' + cleanUrl), T(8000)]);
} else {
await Promise.race([mini.navigateTo('/' + cleanUrl), T(8000)]);
}
} catch (e) {
console.log(` ⚠ 导航超时: ${url} - ${e.message}`);
}
await sleep(2000);
}
async function back() {
try { await Promise.race([mini.navigateBack(), T(5000)]); } catch {}
await sleep(1000);
}
async function tap(selector) {
const page = await curPage();
const el = await page.$(selector);
if (el) { await el.tap(); await sleep(800); return true; }
return false;
}
// ========================================
// 后端准备
// ========================================
console.log('--- 后端准备 ---');
const lr = await api('POST', '/auth/login', { username: 'admin', password: 'Admin@2026' });
const token = lr.data?.data?.access_token;
log('后端', '管理员登录', token ? 'PASS' : 'FAIL', `status=${lr.status}`);
const pr = await api('GET', '/health/patients?page=1&page_size=10', null, token);
const patients = pr.data?.data?.data || [];
log('后端', '患者列表', patients.length > 0 ? 'PASS' : 'WARN', `${patients.length}个患者`);
const patientId = patients[0]?.id;
console.log('');
// ========================================
// 链路1: 登录页 → 首页(通过加密 storage 绕过)
// ========================================
console.log('--- 链路1: 认证流程 ---');
let startPath = await curPath();
log('认证', '初始页面', 'PASS', `路径: ${startPath}`);
if (startPath.includes('login')) {
const loginBtn = await (await curPage()).$('.login-btn, .auth-btn, button, [class*="login"]');
log('认证', '登录按钮', loginBtn ? 'PASS' : 'WARN', loginBtn ? '找到' : '未找到');
// 用 API 获取 admin token加密后写入 storage
try {
const loginRes = await api('POST', '/auth/login', { username: 'admin', password: 'Admin@2026' });
const apiToken = loginRes.data?.data?.access_token;
if (apiToken) {
await mini.callWxMethod('setStorageSync', 'access_token', encrypt(apiToken));
await mini.callWxMethod('setStorageSync', 'refresh_token', encrypt('dummy'));
await mini.callWxMethod('setStorageSync', 'user_data', encrypt(JSON.stringify({ id: 'test', username: 'admin', display_name: '管理员', tenant_id: '019d0da7-a2c1-7820-b0a3-3d5266a3a324' })));
await mini.callWxMethod('setStorageSync', 'user_roles', encrypt(JSON.stringify(['admin'])));
await mini.callWxMethod('setStorageSync', 'tenant_id', encrypt('019d0da7-a2c1-7820-b0a3-3d5266a3a324'));
await mini.callWxMethod('setStorageSync', 'current_patient', { id: patients[0]?.id || 'x', name: patients[0]?.name || '测试', relation: 'self' });
await mini.callWxMethod('setStorageSync', 'current_patient_id', patients[0]?.id || 'x');
log('认证', '加密Token写入', 'PASS', '加密 storage 已设置');
}
await mini.reLaunch('/pages/index/index');
await sleep(4000);
const afterPath = await curPath();
log('认证', 'reLaunch首页', afterPath.includes('index') ? 'PASS' : 'FAIL', `路径: ${afterPath}`);
if (afterPath.includes('login')) {
log('认证', '状态', 'FAIL', '仍在登录页');
} else {
log('认证', '登录成功', 'PASS', `已进入: ${afterPath}`);
}
} catch (e) {
log('认证', '异常', 'FAIL', e.message);
}
}
// ========================================
// 链路2: 健康数据页
// ========================================
console.log('\n--- 链路2: 健康数据 ---');
try {
await nav('/pages/health/index');
const hPath = await curPath();
log('健康数据', '页面导航', hPath.includes('health') ? 'PASS' : 'FAIL', `路径: ${hPath}`);
// API 链路验证
if (token && patientId) {
const tr = await api('GET', `/health/vital-signs/today?patient_id=${patientId}`, null, token);
log('健康数据', '今日体征API(F1)', tr.status === 200 ? 'PASS' : 'FAIL',
`status=${tr.status}, data=${JSON.stringify(tr.data?.data || {}).substring(0, 100)}`);
const trendR = await api('GET', '/health/vital-signs/trend?indicator=weight&range=7d', null, token);
log('健康数据', '趋势API', trendR.status === 200 ? 'PASS' : 'FAIL', `status=${trendR.status}`);
}
// 健康数据是 tabbar 页面,切回首页
await mini.switchTab('/pages/index/index');
await sleep(2000);
} catch (e) {
log('健康数据', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路3: 健康录入
// ========================================
console.log('\n--- 链路3: 健康录入 ---');
try {
await nav('/pages/health/input/index');
const iPath = await curPath();
log('健康录入', '页面导航', iPath.includes('input') ? 'PASS' : 'FAIL', `路径: ${iPath}`);
const page = await curPage();
const inputs = await page.$$('input');
log('健康录入', '表单输入框', inputs.length > 0 ? 'PASS' : 'FAIL', `${inputs.length}`);
// 尝试在第一个数字输入框输入数据
if (inputs.length > 0) {
for (const inp of inputs) {
const type = await inp.attribute('type');
if (type === 'digit' || type === 'number') {
await inp.input('65.5');
log('健康录入', '填写数据', 'PASS', '输入 65.5');
break;
}
}
}
await back();
} catch (e) {
log('健康录入', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路4: 日常监测
// ========================================
console.log('\n--- 链路4: 日常监测 ---');
try {
await nav('/pages/health/daily-monitoring/index');
const dPath = await curPath();
log('日常监测', '页面导航', dPath.includes('daily') ? 'PASS' : 'FAIL', `路径: ${dPath}`);
const page = await curPage();
const dmInputs = await page.$$('.dm-input');
log('日常监测', '表单字段', dmInputs.length > 0 ? 'PASS' : 'FAIL', `${dmInputs.length}个.dm-input`);
// 验证 Zod: 输入超范围值
if (dmInputs.length > 0) {
await dmInputs[0].input('9999');
log('日常监测', 'Zod超范围值', 'PASS', '输入收缩压9999(应被Zod拦截)');
}
const submitBtn = await page.$('.dm-submit');
log('日常监测', '提交按钮', submitBtn ? 'PASS' : 'FAIL', submitBtn ? '找到' : '未找到');
const resetBtn = await page.$('.dm-reset');
log('日常监测', '重置按钮', resetBtn ? 'PASS' : 'WARN', resetBtn ? '找到' : '未找到');
await back();
} catch (e) {
log('日常监测', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路5: 积分商城
// ========================================
console.log('\n--- 链路5: 积分商城 ---');
try {
await nav('/pages/mall/index');
const mPath = await curPath();
log('积分商城', '页面导航', mPath.includes('mall') ? 'PASS' : 'FAIL', `路径: ${mPath}`);
const page = await curPage();
const pointsCard = await page.$('.points-card');
log('积分商城', '积分卡片', pointsCard ? 'PASS' : 'WARN', pointsCard ? '显示积分' : '未显示(可能无档案)');
const checkinBtn = await page.$('.checkin-btn');
log('积分商城', '签到按钮', checkinBtn ? 'PASS' : 'WARN', checkinBtn ? '找到' : '未找到');
const products = await page.$$('.product-card');
log('积分商城', '商品列表', 'PASS', `${products.length}个商品`);
// F2 修复: 无档案降级 UI
const emptyEl = await page.$('.empty-state');
if (emptyEl && !pointsCard) {
log('积分商城', '无档案降级(F2)', 'PASS', '显示降级引导UI');
}
// 积分商城是 tabbar 页面,切回首页
await mini.switchTab('/pages/index/index');
await sleep(2000);
} catch (e) {
log('积分商城', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路6: 预约挂号
// ========================================
console.log('\n--- 链路6: 预约挂号 ---');
try {
await nav('/pages/appointment/index');
const aPath = await curPath();
log('预约', '页面导航', aPath.includes('appointment') ? 'PASS' : 'FAIL', `路径: ${aPath}`);
await back();
} catch (e) {
log('预约', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路7: 家庭成员
// ========================================
console.log('\n--- 链路7: 家庭成员 ---');
try {
await nav('/pages/profile/family/index');
const fPath = await curPath();
log('家庭成员', '页面导航', fPath.includes('family') ? 'PASS' : 'FAIL', `路径: ${fPath}`);
const page = await curPage();
const memberEls = await page.$$('[class*="card"], [class*="member"], [class*="patient"]');
log('家庭成员', '列表渲染', memberEls.length > 0 ? 'PASS' : 'WARN', `${memberEls.length}个元素`);
await back();
} catch (e) {
log('家庭成员', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路8: 咨询
// ========================================
console.log('\n--- 链路8: 咨询 ---');
try {
await nav('/pages/consultation/index');
const cPath = await curPath();
log('咨询', '页面导航', cPath.includes('consultation') ? 'PASS' : 'FAIL', `路径: ${cPath}`);
// 咨询页是 tabbar 页面,不能用 navigateBack切回首页
await mini.switchTab('/pages/index/index');
await sleep(2000);
} catch (e) {
log('咨询', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路9: 文章
// ========================================
console.log('\n--- 链路9: 文章 ---');
try {
await nav('/pages/article/index');
const arPath = await curPath();
log('文章', '页面导航', arPath.includes('article') ? 'PASS' : 'FAIL', `路径: ${arPath}`);
await back();
} catch (e) {
log('文章', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路10: 趋势
// ========================================
console.log('\n--- 链路10: 趋势 ---');
try {
await nav('/pages/health/trend/index');
const trPath = await curPath();
log('趋势', '页面导航', trPath.includes('trend') ? 'PASS' : 'FAIL', `路径: ${trPath}`);
await back();
} catch (e) {
log('趋势', '操作异常', 'FAIL', e.message);
}
// ========================================
// 链路11: 报告
// ========================================
console.log('\n--- 链路11: 报告 ---');
try {
await nav('/pages/profile/reports/index');
const rpPath = await curPath();
log('报告', '页面导航', rpPath.includes('report') ? 'PASS' : 'FAIL', `路径: ${rpPath}`);
await back();
} catch (e) {
log('报告', '操作异常', 'FAIL', e.message);
}
// ========================================
// API 数据闭环验证
// ========================================
console.log('\n--- API 数据闭环 ---');
if (token && patientId) {
const checks = [
['患者详情', 'GET', `/health/patients/${patientId}`],
['预约列表', 'GET', '/health/appointments?page=1&page_size=5'],
['咨询列表', 'GET', '/health/consultation-sessions?page=1&page_size=5'],
['日常监测', 'GET', `/health/patients/${patientId}/daily-monitoring?page=1&page_size=5`],
['积分账户', 'GET', '/health/points/account', 404], // admin 无患者档案, 404 预期
['签到状态', 'GET', '/health/points/checkin/status', 404], // admin 无患者档案, 404 预期
['商品列表', 'GET', '/health/points/products?page=1&page_size=5'],
['医生列表', 'GET', '/health/doctors?page=1&page_size=20'],
['文章列表', 'GET', '/health/articles?page=1&page_size=5&status=published'],
['随访任务', 'GET', '/health/follow-up-tasks?page=1&page_size=5'],
];
for (const check of checks) {
const [label, method, path, expected404] = Array.isArray(check) ? check : [check];
try {
const r = await api(method, path, null, token);
let detail = `status=${r.status}`;
if (r.data?.data?.name) detail += `, name=${r.data.data.name}`;
if (r.data?.data?.total !== undefined) detail += `, total=${r.data.data.total}`;
if (r.data?.data?.data?.length !== undefined) detail += `, items=${r.data.data.data.length}`;
if (r.status === 200) {
log('API闭环', label, 'PASS', detail);
} else if (r.status === 404 && expected404) {
log('API闭环', label, 'WARN', `${detail} (预期:无档案/无路由)`);
} else {
log('API闭环', label, 'FAIL', detail);
}
} catch (e) {
log('API闭环', label, 'FAIL', e.message);
}
}
}
// ====== 断开 ======
await mini.disconnect();
// ====== 汇总 ======
console.log('\n\n========================================');
console.log(' HMS 小程序端到端链路验证报告');
console.log('========================================\n');
const chains = [...new Set(results.map(r => r.chain))];
for (const chain of chains) {
const items = results.filter(r => r.chain === chain);
const p = items.filter(r => r.status === 'PASS').length;
const f = items.filter(r => r.status === 'FAIL').length;
const w = items.filter(r => r.status === 'WARN').length;
const icon = f > 0 ? '❌' : w > 0 ? '⚠️' : '✅';
console.log(`${icon} ${chain}: ${p}通过/${f}失败/${w}警告`);
for (const item of items.filter(i => i.status !== 'PASS')) {
console.log(` ${item.status === 'FAIL' ? '❌' : '⚠️'} ${item.step}: ${item.detail}`);
}
}
const tp = results.filter(r => r.status === 'PASS').length;
const tf = results.filter(r => r.status === 'FAIL').length;
const tw = results.filter(r => r.status === 'WARN').length;
console.log(`\n总计: ${results.length}项 — ✅${tp}通过 / ❌${tf}失败 / ⚠️${tw}警告`);
console.log('========================================\n');
process.exit(tf > 0 ? 1 : 0);
}
main().catch(e => { console.error('致命错误:', e); process.exit(1); });