/** * 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); });