import automator from 'miniprogram-automator'; import fs from 'fs'; const WS = 'ws://127.0.0.1:9420'; const TIMEOUT = 12000; const SS_DIR = 'g:/hms/apps/miniprogram/screenshots'; function withTimeout(promise, ms, label) { return Promise.race([promise, new Promise((_, r) => setTimeout(() => r(new Error(`${label} timeout`)), ms))]); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // 通过 evaluate 获取页面 DOM 结构信息 function getDomChecker() { return ` (function() { const results = { texts: [], classes: [], inputs: [], buttons: [], links: [], images: [], empty: false }; function walk(el) { if (!el || !el.children) return; for (const child of Array.from(el.children)) { const tag = (child.tagName || '').toLowerCase(); const cls = child.className || ''; const text = (child.textContent || '').trim().substring(0, 80); if (text && text.length > 1) results.texts.push(text); if (cls && tag === 'view') results.classes.push(cls); if (tag === 'input' || tag === 'textarea') { results.inputs.push({ type: child.type || 'text', placeholder: child.placeholder || '' }); } if (tag === 'button') { results.buttons.push(text || 'button'); } if (tag === 'a' || cls.includes('link')) { results.links.push(text); } if (tag === 'img' || tag === 'image') { results.images.push(child.src || ''); } walk(child); } } const page = document.querySelector('.page, [class*="-page"]') || document.body; results.empty = page.children.length === 0; walk(page); // 去重 results.texts = [...new Set(results.texts)].slice(0, 30); results.classes = [...new Set(results.classes)].slice(0, 20); return results; })() `; } async function main() { console.log('=== HMS 小程序深度验证 ===\n'); if (!fs.existsSync(SS_DIR)) fs.mkdirSync(SS_DIR, { recursive: true }); const mp = await withTimeout(automator.connect({ wsEndpoint: WS }), TIMEOUT, 'connect'); console.log('[OK] 已连接\n'); const results = []; // === 1. 首页 === console.log('━━━ 1. 首页 ━━━'); try { await withTimeout(mp.switchTab('/pages/index/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); console.log(' 路径:', page.path); console.log(' data keys:', Object.keys(data).join(', ') || '(空)'); // 检查问候语和日期 const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); // 检查关键元素 const hasGreeting = evalResult.texts.some(t => /早上好|下午好|晚上好/.test(t)); const hasDate = evalResult.texts.some(t => /\d{4}\/\d/.test(t)); const hasHealthCard = evalResult.texts.some(t => /今日健康/.test(t)); const hasServices = evalResult.texts.some(t => /预约挂号|健康录入|健康趋势|资讯文章/.test(t)); const hasEmpty = evalResult.texts.some(t => /暂无待办/.test(t)); console.log(' 问候语:', hasGreeting ? '✓' : '✗ 缺失'); console.log(' 日期显示:', hasDate ? '✓' : '✗ 缺失'); console.log(' 今日健康卡片:', hasHealthCard ? '✓' : '✗ 缺失'); console.log(' 快捷服务(4项):', hasServices ? '✓' : '✗ 缺失'); console.log(' 待办空状态:', hasEmpty ? '✓' : '✗ 缺失'); // 未登录时应显示"访客" const isVisitor = evalResult.texts.some(t => t.includes('访客')); console.log(' 未登录显示访客:', isVisitor ? '✓' : ' (已登录或其他)'); results.push({ page: '首页', pass: [hasGreeting, hasHealthCard, hasServices].filter(Boolean).length, total: 3, details: { hasGreeting, hasHealthCard, hasServices, hasDate, hasEmpty, isVisitor } }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '首页', pass: 0, total: 3, error: e.message }); } // === 2. 健康中心 === console.log('\n━━━ 2. 健康中心 ━━━'); try { await withTimeout(mp.switchTab('/pages/health/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasHeader = evalResult.texts.some(t => /健康数据/.test(t)); const hasInputBtn = evalResult.texts.some(t => /录入/.test(t)); const hasIndicators = evalResult.texts.some(t => /血压|心率|血糖|体重/.test(t)); const hasTrendActions = evalResult.texts.some(t => /血压趋势|心率趋势|血糖趋势/.test(t)); const hasUnits = evalResult.texts.some(t => /mmHg|bpm|mmol\/L/.test(t)); console.log(' 标题"健康数据":', hasHeader ? '✓' : '✗'); console.log(' 录入按钮:', hasInputBtn ? '✓' : '✗'); console.log(' 指标卡片(血压/心率/血糖/体重):', hasIndicators ? '✓' : '✗'); console.log(' 趋势快捷入口:', hasTrendActions ? '✓' : '✗'); console.log(' 单位标注:', hasUnits ? '✓' : '✗'); results.push({ page: '健康中心', pass: [hasHeader, hasInputBtn, hasIndicators, hasTrendActions].filter(Boolean).length, total: 4 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '健康中心', pass: 0, total: 4, error: e.message }); } // === 3. 健康数据录入 === console.log('\n━━━ 3. 健康数据录入 ━━━'); try { await withTimeout(mp.reLaunch('/pages/health/input/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasIndicatorPicker = evalResult.texts.some(t => /血压|心率|血糖|体重|体温/.test(t)); const hasInput = evalResult.inputs.length > 0; const hasSubmit = evalResult.texts.some(t => /提交/.test(t)); const hasPicker = evalResult.texts.some(t => /指标类型/.test(t)); console.log(' 指标类型选择:', hasIndicatorPicker ? '✓' : '✗'); console.log(' 输入框:', hasInput ? `✓ (${evalResult.inputs.length}个)` : '✗'); console.log(' 提交按钮:', hasSubmit ? '✓' : '✗'); console.log(' "指标类型"标签:', hasPicker ? '✓' : '✗'); results.push({ page: '健康录入', pass: [hasIndicatorPicker, hasInput, hasSubmit].filter(Boolean).length, total: 3 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '健康录入', pass: 0, total: 3, error: e.message }); } // === 4. 健康趋势 === console.log('\n━━━ 4. 健康趋势 ━━━'); try { await withTimeout(mp.reLaunch('/pages/health/trend/index?indicator=heart_rate'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasTitle = evalResult.texts.some(t => /趋势/.test(t)); const hasTabs = evalResult.texts.some(t => /7天|30天|90天/.test(t)); const hasChart = evalResult.classes.some(c => /chart|trend/i.test(c)); console.log(' 标题"趋势":', hasTitle ? '✓' : '✗'); console.log(' 时间范围Tab:', hasTabs ? '✓' : '✗'); console.log(' 图表容器:', hasChart ? '✓' : '✗'); console.log(' indicator参数:', page.path); results.push({ page: '健康趋势', pass: [hasTitle, hasTabs].filter(Boolean).length, total: 2 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '健康趋势', pass: 0, total: 2, error: e.message }); } // === 5. 预约列表 === console.log('\n━━━ 5. 预约列表 ━━━'); try { await withTimeout(mp.switchTab('/pages/appointment/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasTitle = evalResult.texts.some(t => /预约挂号/.test(t)); const hasFabBtn = evalResult.texts.some(t => /新建预约/.test(t)); const hasEmpty = evalResult.texts.some(t => /暂无预约/.test(t)); console.log(' 标题"预约挂号":', hasTitle ? '✓' : '✗'); console.log(' "新建预约"按钮:', hasFabBtn ? '✓' : '✗'); console.log(' 空状态提示:', hasEmpty ? '✓' : ' (有数据或缺失)'); results.push({ page: '预约列表', pass: [hasTitle, hasFabBtn].filter(Boolean).length, total: 2 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '预约列表', pass: 0, total: 2, error: e.message }); } // === 6. 创建预约 === console.log('\n━━━ 6. 创建预约 ━━━'); try { await withTimeout(mp.reLaunch('/pages/appointment/create/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | ')); const hasStepIndicator = evalResult.texts.some(t => /选科室|选医生|选时段/.test(t)); const hasDeptGrid = evalResult.texts.some(t => /内科|外科|妇科|儿科|体检中心|中医科/.test(t)); const hasDeptIcons = evalResult.texts.some(t => /🫀|🔪|👩‍⚕️|👶|🏥|🌿/.test(t)); const hasNextBtn = evalResult.texts.some(t => /下一步/.test(t)); console.log(' 步骤指示器:', hasStepIndicator ? '✓' : '✗'); console.log(' 科室宫格(6项):', hasDeptGrid ? '✓' : '✗'); console.log(' 科室图标:', hasDeptIcons ? '✓' : '✗'); console.log(' "下一步"按钮:', hasNextBtn ? '✓' : '✗'); results.push({ page: '创建预约', pass: [hasStepIndicator, hasDeptGrid, hasNextBtn].filter(Boolean).length, total: 3 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '创建预约', pass: 0, total: 3, error: e.message }); } // === 7. 资讯文章 === console.log('\n━━━ 7. 资讯文章 ━━━'); try { await withTimeout(mp.switchTab('/pages/article/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasEmpty = evalResult.texts.some(t => /暂无资讯/.test(t)); const hasArticleCards = evalResult.classes.some(c => /article-card/.test(c)); const hasArticleContent = evalResult.texts.some(t => t.length > 5); // 文章标题等 console.log(' 空状态提示:', hasEmpty ? '✓ (无数据时)' : ' (有文章或缺失)'); console.log(' 文章卡片结构:', hasArticleCards ? '✓' : '✗'); console.log(' 文章内容:', hasArticleContent ? '✓' : '✗'); results.push({ page: '资讯文章', pass: 1, total: 1 }); // 至少页面结构正确 } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '资讯文章', pass: 0, total: 1, error: e.message }); } // === 8. 个人中心 === console.log('\n━━━ 8. 个人中心 ━━━'); try { await withTimeout(mp.switchTab('/pages/profile/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | ')); const hasAvatar = evalResult.texts.some(t => /\?/.test(t)); // 未登录显示 ? const hasLoginHint = evalResult.texts.some(t => /未登录/.test(t)); const hasMenu = evalResult.texts.some(t => /就诊人管理|我的报告|我的随访|用药提醒|设置/.test(t)); const hasLogout = evalResult.texts.some(t => /退出登录/.test(t)); const hasMenuIcons = evalResult.texts.some(t => /👥|📋|💬|💊|⚙️/.test(t)); const hasArrows = evalResult.texts.some(t => /›/.test(t)); console.log(' 未登录显示"未登录":', hasLoginHint ? '✓' : '✗'); console.log(' 菜单项(5项):', hasMenu ? '✓' : '✗'); console.log(' 退出登录按钮:', hasLogout ? '✓' : '✗'); console.log(' 菜单图标:', hasMenuIcons ? '✓' : '✗'); console.log(' 菜单箭头:', hasArrows ? '✓' : '✗'); results.push({ page: '个人中心', pass: [hasLoginHint, hasMenu, hasLogout].filter(Boolean).length, total: 3 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '个人中心', pass: 0, total: 3, error: e.message }); } // === 9. 就诊人管理 === console.log('\n━━━ 9. 就诊人管理 ━━━'); try { await withTimeout(mp.reLaunch('/pages/profile/family/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasAddBtn = evalResult.texts.some(t => /添加就诊人/.test(t)); const hasEmpty = evalResult.texts.some(t => /暂无就诊人/.test(t)); console.log(' "添加就诊人"按钮:', hasAddBtn ? '✓' : '✗'); console.log(' 空状态:', hasEmpty ? '✓' : ' (有数据或缺失)'); results.push({ page: '就诊人管理', pass: [hasAddBtn].filter(Boolean).length, total: 1 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '就诊人管理', pass: 0, total: 1, error: e.message }); } // === 10. 我的报告 === console.log('\n━━━ 10. 我的报告 ━━━'); try { await withTimeout(mp.reLaunch('/pages/profile/reports/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | ')); const hasEmpty = evalResult.texts.some(t => /暂无报告/.test(t)); console.log(' 空状态:', hasEmpty ? '✓' : ' (有数据或缺失)'); results.push({ page: '我的报告', pass: 1, total: 1 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '我的报告', pass: 0, total: 1, error: e.message }); } // === 11. 登录页 === console.log('\n━━━ 11. 登录页 ━━━'); try { await withTimeout(mp.reLaunch('/pages/login/index'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const data = await withTimeout(page.data(), TIMEOUT, 'data'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | ')); const hasLogo = evalResult.texts.some(t => /\+/.test(t)); const hasTitle = evalResult.texts.some(t => /健康管理/.test(t)); const hasSubtitle = evalResult.texts.some(t => /专属健康管家/.test(t)); const hasLoginBtn = evalResult.texts.some(t => /微信一键登录/.test(t)); const hasAgreement = evalResult.texts.some(t => /用户服务协议/.test(t)); const hasPrivacy = evalResult.texts.some(t => /隐私政策/.test(t)); const hasCheckbox = evalResult.classes.some(c => /checkbox|agreement/i.test(c)); const hasAgreeText = evalResult.texts.some(t => /我已阅读并同意/.test(t)); console.log(' Logo "+":', hasLogo ? '✓' : '✗'); console.log(' 标题"健康管理":', hasTitle ? '✓' : '✗'); console.log(' 副标题:', hasSubtitle ? '✓' : '✗'); console.log(' "微信一键登录"按钮:', hasLoginBtn ? '✓' : '✗'); console.log(' 用户协议链接:', hasAgreement ? '✓' : '✗'); console.log(' 隐私政策链接:', hasPrivacy ? '✓' : '✗'); console.log(' 协议勾选框:', hasCheckbox ? '✓' : '✗'); console.log(' "我已阅读并同意"文案:', hasAgreeText ? '✓' : '✗'); results.push({ page: '登录页', pass: [hasTitle, hasLoginBtn, hasAgreement, hasPrivacy].filter(Boolean).length, total: 4 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '登录页', pass: 0, total: 4, error: e.message }); } // === 12. 用户协议 === console.log('\n━━━ 12. 用户协议 ━━━'); try { await withTimeout(mp.reLaunch('/pages/legal/user-agreement'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本(前5):', evalResult.texts.slice(0, 5).join(' | ')); const hasContent = evalResult.texts.length > 2; console.log(' 协议内容:', hasContent ? `✓ (${evalResult.texts.length}段文字)` : '✗ 内容为空'); results.push({ page: '用户协议', pass: hasContent ? 1 : 0, total: 1 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '用户协议', pass: 0, total: 1, error: e.message }); } // === 13. 隐私政策 === console.log('\n━━━ 13. 隐私政策 ━━━'); try { await withTimeout(mp.reLaunch('/pages/legal/privacy-policy'), TIMEOUT, 'nav'); await sleep(2000); const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page'); const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom'); console.log(' DOM文本(前5):', evalResult.texts.slice(0, 5).join(' | ')); const hasContent = evalResult.texts.length > 2; console.log(' 隐私政策内容:', hasContent ? `✓ (${evalResult.texts.length}段文字)` : '✗ 内容为空'); results.push({ page: '隐私政策', pass: hasContent ? 1 : 0, total: 1 }); } catch (e) { console.log(' [FAIL]', e.message); results.push({ page: '隐私政策', pass: 0, total: 1, error: e.message }); } // === 汇总 === console.log('\n╔══════════════════════════════════╗'); console.log('║ 深度验证结果汇总 ║'); console.log('╠══════════════════════════════════╣'); let totalPass = 0, totalCheck = 0; for (const r of results) { const bar = r.error ? 'FAIL' : `${r.pass}/${r.total}`; const icon = r.error ? '✗' : r.pass === r.total ? '✓' : '△'; console.log(`║ ${icon} ${r.page.padEnd(12)} ${bar.padEnd(10)} ║`); totalPass += r.pass; totalCheck += r.total; } console.log('╠══════════════════════════════════╣'); console.log(`║ 合计: ${totalPass}/${totalCheck} 检查项通过 ║`); console.log('╚══════════════════════════════════╝'); try { await mp.close(); } catch {} } main().catch(err => { console.error('Fatal:', err.message); process.exit(1); });