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