@@ -0,0 +1,419 @@
/**
* 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 ) ; } ) ;