Files
hms/apps/miniprogram/e2e-chain-v3.cjs
iven 50eae8b809 feat(miniprogram): 温润东方风全面 UI 重设计
73 文件变更,覆盖全部 40 个页面 SCSS + TabBar 图标 + 组件样式。
统一赤陶主色 #C4623A + 暖米背景 + 衬线标题字体 + 12px 圆角体系。
2026-04-28 00:19:52 +08:00

273 lines
11 KiB
JavaScript

/**
* HMS 小程序端到端链路验证 v3
* 使用 pageStack + 超时保护避免卡死
*/
const automator = require('miniprogram-automator');
const http = require('http');
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();
});
}
function timeout(ms) { return new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms)); }
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function race(p, ms, label) { return Promise.race([p, timeout(ms)]).catch(e => ({ _err: label + ': ' + e.message })); }
async function getPage(mini) {
// pageStack 可能在某些状态下卡住,改用 screenshot + evaluate 来验证
try {
const stack = await Promise.race([mini.pageStack(), timeout(3000)]);
if (Array.isArray(stack) && stack.length > 0) {
const last = stack[stack.length - 1];
try {
const p = await Promise.race([last.path, timeout(2000)]);
return { page: last, path: typeof p === 'string' ? p : 'unknown' };
} catch { return { page: last, path: 'ok' }; }
}
} catch {}
return { path: 'stack_timeout' };
}
async function nav(mini, url) {
const r = await race(mini.navigateTo({ url }), 5000, 'nav');
await sleep(2000);
return !r._err;
}
async function back(mini) {
await race(mini.navigateBack(), 3000, 'back');
await sleep(1000);
}
async function main() {
console.log('\n=== HMS 小程序端到端链路验证 v3 ===\n');
// 连接
let mini;
try {
mini = await Promise.race([automator.connect({ wsEndpoint: 'ws://localhost:9420' }), timeout(10000)]);
console.log('连接成功!\n');
} catch (e) {
console.error('连接失败:', e.message);
process.exit(1);
}
// ====== 后端健康检查 ======
console.log('--- 后端健康检查 ---');
let token, patients;
try {
const lr = await api('POST', '/auth/login', { username: 'admin', password: 'Admin@2026' });
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);
patients = pr.data?.data?.data || [];
log('后端', '患者列表', patients.length > 0 ? 'PASS' : 'WARN', `${patients.length}`);
} catch (e) {
log('后端', '检查', 'FAIL', e.message);
}
// ====== 链路1: 首页 ======
console.log('\n--- 链路1: 首页 ---');
const home = await getPage(mini);
log('首页', '页面', 'PASS', `路径: ${home.path}`);
// ====== 链路2: 健康数据 ======
console.log('\n--- 链路2: 健康数据 ---');
if (await nav(mini, '/pages/health/index')) {
const h = await getPage(mini);
log('健康数据', '页面加载', h.path.includes('health') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
if (token && patients?.[0]) {
const tr = await api('GET', `/health/vital-signs/today?patient_id=${patients[0].id}`, null, token);
log('健康数据', '今日体征(F1)', tr.status === 200 ? 'PASS' : 'FAIL', `status=${tr.status}`);
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}`);
}
await back(mini);
}
// ====== 链路3: 健康录入 ======
console.log('\n--- 链路3: 健康录入 ---');
if (await nav(mini, '/pages/health/input/index')) {
const h = await getPage(mini);
log('健康录入', '页面加载', h.path.includes('input') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
if (h.page) {
const inputs = await race(h.page.$$('input'), 3000, 'inputs');
log('健康录入', '输入框', !inputs?._err && inputs?.length > 0 ? 'PASS' : 'WARN', `${inputs?.length || 0}`);
}
await back(mini);
}
// ====== 链路4: 日常监测 ======
console.log('\n--- 链路4: 日常监测 ---');
if (await nav(mini, '/pages/health/daily-monitoring/index')) {
const h = await getPage(mini);
log('日常监测', '页面加载', h.path.includes('daily') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
if (h.page) {
const inputs = await race(h.page.$$('.dm-input'), 3000, 'inputs');
log('日常监测', '表单字段(M6)', !inputs?._err && inputs?.length > 0 ? 'PASS' : 'WARN', `${inputs?.length || 0}`);
const btn = await race(h.page.$('.dm-submit'), 3000, 'btn');
log('日常监测', '提交按钮', !btn?._err && btn ? 'PASS' : 'WARN', btn ? '找到' : '未找到');
}
await back(mini);
}
// ====== 链路5: 积分商城 ======
console.log('\n--- 链路5: 积分商城 ---');
if (await nav(mini, '/pages/mall/index')) {
const h = await getPage(mini);
log('积分商城', '页面加载', h.path.includes('mall') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
if (h.page) {
const pc = await race(h.page.$('.points-card'), 3000, 'points');
log('积分商城', '积分卡片', !pc?._err && pc ? 'PASS' : 'WARN', pc ? '找到' : '未找到');
const cb = await race(h.page.$('.checkin-btn'), 3000, 'checkin');
log('积分商城', '签到按钮', !cb?._err && cb ? 'PASS' : 'WARN', cb ? '找到' : '未找到');
const prods = await race(h.page.$$('.product-card'), 3000, 'prods');
log('积分商城', '商品列表', 'PASS', `${prods?.length || 0}个商品`);
const empty = await race(h.page.$('.empty-state'), 3000, 'empty');
if (!empty?._err && empty) {
log('积分商城', '无档案降级(F2)', 'PASS', '显示降级UI');
}
}
await back(mini);
}
// ====== 链路6: 预约挂号 ======
console.log('\n--- 链路6: 预约挂号 ---');
if (await nav(mini, '/pages/health/appointment/index')) {
const h = await getPage(mini);
log('预约', '页面加载', h.path.includes('appointment') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== 链路7: 家庭成员 ======
console.log('\n--- 链路7: 家庭成员 ---');
if (await nav(mini, '/pages/profile/family/index')) {
const h = await getPage(mini);
log('家庭成员', '页面加载', h.path.includes('family') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== 链路8: 咨询 ======
console.log('\n--- 链路8: 咨询 ---');
if (await nav(mini, '/pages/health/consultation/index')) {
const h = await getPage(mini);
log('咨询', '页面加载', h.path.includes('consultation') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== 链路9: 文章 ======
console.log('\n--- 链路9: 文章 ---');
if (await nav(mini, '/pages/health/articles/index')) {
const h = await getPage(mini);
log('文章', '页面加载', h.path.includes('article') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== 链路10: 趋势 ======
console.log('\n--- 链路10: 趋势 ---');
if (await nav(mini, '/pages/health/trend/index')) {
const h = await getPage(mini);
log('趋势', '页面加载', h.path.includes('trend') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== 链路11: 报告 ======
console.log('\n--- 链路11: 报告 ---');
if (await nav(mini, '/pages/health/reports/index')) {
const h = await getPage(mini);
log('报告', '页面加载', h.path.includes('report') ? 'PASS' : 'FAIL', `路径: ${h.path}`);
await back(mini);
}
// ====== API 数据闭环 ======
console.log('\n--- API 数据闭环 ---');
if (token && patients?.[0]) {
const pid = patients[0].id;
const checks = [
['患者详情', 'GET', `/health/patients/${pid}`],
['预约列表', 'GET', '/health/appointments?page=1&page_size=5'],
['咨询列表', 'GET', '/health/consultation-sessions?page=1&page_size=5'],
['日常监测', 'GET', `/health/patients/${pid}/daily-monitoring?page=1&page_size=5`],
['积分账户', 'GET', '/health/points/account'],
['签到状态', 'GET', '/health/points/checkin/status'],
['商品列表', 'GET', '/health/points/products?page=1&page_size=5'],
];
for (const [label, method, path] of checks) {
try {
const r = await api(method, path, null, token);
const ok = r.status === 200;
const detail = r.data?.data?.name ? `${label}: ${r.data.data.name}` :
r.data?.data?.total !== undefined ? `${label}: total=${r.data.data.total}` :
`status=${r.status}`;
log('API闭环', label, ok ? 'PASS' : 'FAIL', detail);
} catch (e) {
log('API闭环', label, 'FAIL', e.message);
}
}
}
// ---- 关闭 ----
try { await mini.disconnect(); } catch {}
try { await mini.close(); } catch {}
// ---- 汇总 ----
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); });