Browse Source

test: 补强病案复印入口交互契约

anubis 1 month ago
parent
commit
066944bfec

+ 143 - 27
04-新项目源码_source/项目源码/pri-smart-hospital-miniprogram/tests/mrc-apply-entry-interaction.test.js

@@ -1,49 +1,165 @@
-const assert = require('assert');
 const fs = require('fs');
 const path = require('path');
 
 const root = path.join(__dirname, '..');
+const applyEntryDir = path.join(root, 'packageMRC', 'pages', 'apply-entry');
+const applyEntryScriptPath = path.join(applyEntryDir, 'index.js');
+const applyEntryTemplatePath = path.join(applyEntryDir, 'index.wxml');
 const patientCreateDir = path.join(root, 'packageMRC', 'pages', 'patient-create');
 const patientCreateScriptPath = path.join(patientCreateDir, 'index.js');
 const patientCreateTemplatePath = path.join(patientCreateDir, 'index.wxml');
 const adapterPath = path.join(root, 'packageMRC', 'common', 'mrcDemoState.js');
 const demoState = require(adapterPath);
+const failures = [];
 
-function readRequired(filePath, intent) {
-  assert(fs.existsSync(filePath), intent);
-  return fs.readFileSync(filePath, 'utf8');
+function expect(condition, message) {
+  if (!condition) failures.push(message);
+}
+
+function readOptional(filePath) {
+  return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
 }
 
 function assertNoForbiddenCapability(source, scope) {
-  assert(!/\bwx\.(?:request|upload\w*|choose(?:Image|Media|MessageFile)|getStorage(?:Sync)?|setStorage(?:Sync)?|removeStorage(?:Sync)?|clearStorage(?:Sync)?)\b/.test(source), `${scope}不得使用 wx.request、上传能力或 Storage`);
-  assert(!/(?:utils\/(?:patientMaintenance|patientSelector|homePatient)|pages\/mine\/patientEdit)/.test(source), `${scope}不得引用既有就诊人服务或页面`);
+  expect(!/\bwx\.(?:request|upload\w*|choose(?:Image|Media|MessageFile)|getStorage(?:Sync)?|setStorage(?:Sync)?|removeStorage(?:Sync)?|clearStorage(?:Sync)?)\b/.test(source), `${scope}不得使用 wx.request、上传能力或 Storage`);
+  expect(!/(?:utils\/(?:patientMaintenance|patientSelector|homePatient)|pages\/mine\/patientEdit)/.test(source), `${scope}不得引用既有就诊人服务或页面`);
+}
+
+function handlerForClass(template, className, interactionName) {
+  const tag = new RegExp(`<[^>]*class=["'][^"']*\\b${className}\\b[^"']*["'][^>]*>`, 'g').exec(template);
+  expect(Boolean(tag), `${interactionName}必须保留 .${className} 可点击节点`);
+  if (!tag) return '';
+  const binding = /bindtap=["']([\w$]+)["']/.exec(tag[0]);
+  expect(Boolean(binding), `${interactionName}必须绑定 tap 处理函数`);
+  return binding ? binding[1] : '';
+}
+
+function loadApplyEntryPage() {
+  let definition;
+  const originalPage = global.Page;
+  global.Page = (config) => { definition = config; };
+  delete require.cache[require.resolve(applyEntryScriptPath)];
+  try {
+    require(applyEntryScriptPath);
+  } catch (error) {
+    failures.push(`申请入口 JS 必须可由 Page mock 加载:${error.message}`);
+  } finally {
+    global.Page = originalPage;
+  }
+  expect(Boolean(definition), '申请入口必须通过 Page 注册,供真实小程序事件分发');
+  return definition;
 }
 
-const patientCreateScript = readRequired(patientCreateScriptPath, '本地 Mock 添加就诊人页必须存在,当前交互契约应保持 RED 直至该页实现');
-const patientCreateTemplate = readRequired(patientCreateTemplatePath, '本地 Mock 添加就诊人页必须提供可操作表单');
-const adapterSource = fs.readFileSync(adapterPath, 'utf8');
+function mountPage(definition) {
+  const page = {
+    data: Object.assign({}, definition.data || {}),
+    setData(nextData) { Object.assign(this.data, nextData || {}); }
+  };
+  Object.keys(definition).forEach((key) => {
+    if (typeof definition[key] === 'function') page[key] = definition[key];
+  });
+  return page;
+}
+
+function invoke(page, handlerName, event, interactionName) {
+  if (!handlerName || typeof page[handlerName] !== 'function') {
+    failures.push(`${interactionName}的 ${handlerName || 'tap'} 处理函数必须在申请入口 Page 中实现`);
+    return undefined;
+  }
+  try {
+    return page[handlerName].call(page, event || {});
+  } catch (error) {
+    failures.push(`${interactionName}必须可在 Page mock 中执行:${error.message}`);
+    return undefined;
+  }
+}
+
+const applyEntryTemplate = readOptional(applyEntryTemplatePath);
+expect(Boolean(applyEntryTemplate), '申请入口 WXML 必须存在');
+const serviceHandler = handlerForClass(applyEntryTemplate, 'service-btn', '服务 CTA');
+const applicationHandler = handlerForClass(applyEntryTemplate, 'my-application-card', '我的申请');
+const patientSelectorHandler = handlerForClass(applyEntryTemplate, 'patient-selector', '就诊人选择');
+const patientSheetHandler = handlerForClass(applyEntryTemplate, 'sheet-item', '就诊人抽屉选项');
+const demoEntryHandler = handlerForClass(applyEntryTemplate, 'demo-mode-entry', 'Demo 模式入口');
+const demoCloseHandler = handlerForClass(applyEntryTemplate, 'demo-mode-close', 'Demo 模式关闭');
+const patientAddHandler = handlerForClass(applyEntryTemplate, 'patient-add-card', '无就诊人添加');
+
+const originalWx = global.wx;
+const navigations = [];
+global.wx = {
+  navigateTo(options) { navigations.push(options || {}); },
+  showToast() {}
+};
 
-assert(/bindinput=/.test(patientCreateTemplate), '添加就诊人页必须保留原型表单的真实输入交互');
-assert(/createAnonymousPatient/.test(patientCreateScript), '添加就诊人页必须仅通过 Demo Adapter 创建匿名患者');
-assert.strictEqual(typeof demoState.createAnonymousPatient, 'function', 'Demo Adapter 必须提供 createAnonymousPatient,且不能写入原始表单值');
+try {
+  const definition = loadApplyEntryPage();
+  if (definition) {
+    const page = mountPage(definition);
+    invoke(page, patientAddHandler, {}, '无就诊人添加');
+    expect(navigations.some((item) => item.url === '/packageMRC/pages/patient-create/index'), '无就诊人添加必须实际导航到本地 patient-create 页面');
+
+    invoke(page, demoEntryHandler, {}, 'Demo 模式入口');
+    expect(page.data.showDemoMode === true, '点击 Demo 模式入口必须打开辅助面板');
+    invoke(page, demoCloseHandler, {}, 'Demo 模式关闭');
+    expect(page.data.showDemoMode === false, '关闭 Demo 模式必须恢复患者首屏');
+
+    if (typeof demoState.createAnonymousPatient === 'function') {
+      const created = demoState.createAnonymousPatient({
+        name: 'PATIENT_INPUT_NAME_MUST_NOT_PERSIST',
+        idCard: 'PATIENT_INPUT_ID_MUST_NOT_PERSIST',
+        mobile: 'PATIENT_INPUT_MOBILE_MUST_NOT_PERSIST'
+      });
+      expect(created && created.ok, 'Demo Adapter 必须能生成匿名演示就诊人');
+      invoke(page, 'onShow', {}, 'patient-create 返回后的入口刷新');
+      expect(page.data.currentPatient && /演示就诊人/.test(page.data.currentPatient.displayName || ''), 'patient-create 返回后 onShow 必须回填当前匿名就诊人');
+
+      invoke(page, patientSelectorHandler, {}, '就诊人选择');
+      expect(page.data.showPatientSheet === true, '点击当前就诊人必须打开底部选择抽屉');
+      const candidate = (page.data.patients || []).find((patient) => patient.id !== (page.data.currentPatient || {}).id);
+      expect(Boolean(candidate), '本地演示必须提供至少两名匿名就诊人供抽屉选择');
+      if (candidate) {
+        invoke(page, patientSheetHandler, { currentTarget: { dataset: { id: candidate.id } } }, '就诊人抽屉选项');
+        expect(page.data.showPatientSheet === false, '选择就诊人后必须关闭底部抽屉');
+        expect((page.data.currentPatient || {}).id === candidate.id, '选择就诊人后必须回填当前就诊人卡片');
+      }
+
+      invoke(page, serviceHandler, {}, '服务 CTA');
+      expect(navigations.some((item) => /^\/packageMRC\/pages\/identity\/index(?:\?|$)/.test(item.url || '')), '有匿名就诊人时“我要申请复印”必须进入身份验证页');
+    } else {
+      failures.push('Demo Adapter 必须提供 createAnonymousPatient,且不能写入原始表单值');
+    }
+
+    invoke(page, applicationHandler, {}, '我的申请');
+    expect(navigations.some((item) => item.url === '/packageMRC/pages/my-applications/index'), '点击“我的申请”必须进入申请列表页');
+  }
+} finally {
+  global.wx = originalWx;
+}
+
+const patientCreateScript = readOptional(patientCreateScriptPath);
+const patientCreateTemplate = readOptional(patientCreateTemplatePath);
+const adapterSource = readOptional(adapterPath);
+expect(Boolean(patientCreateScript), '本地 Mock 添加就诊人页必须存在,当前交互契约应保持 RED 直至该页实现');
+expect(Boolean(patientCreateTemplate), '本地 Mock 添加就诊人页必须提供可操作表单');
+expect(/bindinput=/.test(patientCreateTemplate), '添加就诊人页必须保留原型表单的真实输入交互');
+expect(/createAnonymousPatient/.test(patientCreateScript), '添加就诊人页必须仅通过 Demo Adapter 创建匿名患者');
+expect(typeof demoState.createAnonymousPatient === 'function', 'Demo Adapter 必须提供 createAnonymousPatient,且不能写入原始表单值');
 
 assertNoForbiddenCapability(patientCreateScript, '本地 Mock 添加就诊人页');
 assertNoForbiddenCapability(adapterSource, 'MRC Demo Adapter');
 
-const rawInput = {
-  name: 'PATIENT_INPUT_NAME_MUST_NOT_PERSIST',
-  idCard: 'PATIENT_INPUT_ID_MUST_NOT_PERSIST',
-  mobile: 'PATIENT_INPUT_MOBILE_MUST_NOT_PERSIST'
-};
-const createResult = demoState.createAnonymousPatient(rawInput);
-assert(createResult && createResult.ok, 'Demo Adapter 必须能生成匿名演示就诊人');
-
-const snapshotResult = demoState.getSnapshot();
-assert(snapshotResult && snapshotResult.ok, '匿名患者创建后必须能读取 Demo Adapter 快照');
-const snapshotText = JSON.stringify(snapshotResult.data);
-Object.keys(rawInput).forEach((field) => {
-  assert(!snapshotText.includes(rawInput[field]), `Adapter 快照不得包含 patient-create 的原始 ${field} 输入`);
-});
-assert(/演示就诊人/.test(snapshotText), 'Adapter 快照只能暴露生成后的匿名演示就诊人');
+if (typeof demoState.createAnonymousPatient === 'function') {
+  const snapshotResult = demoState.getSnapshot();
+  expect(snapshotResult && snapshotResult.ok, '匿名患者创建后必须能读取 Demo Adapter 快照');
+  const snapshotText = JSON.stringify(snapshotResult && snapshotResult.data);
+  ['PATIENT_INPUT_NAME_MUST_NOT_PERSIST', 'PATIENT_INPUT_ID_MUST_NOT_PERSIST', 'PATIENT_INPUT_MOBILE_MUST_NOT_PERSIST'].forEach((input) => {
+    expect(!snapshotText.includes(input), `Adapter 快照不得包含 patient-create 的原始输入:${input}`);
+  });
+  expect(/演示就诊人/.test(snapshotText), 'Adapter 快照只能暴露生成后的匿名演示就诊人');
+}
+
+if (failures.length) {
+  throw new Error(`mrc-apply-entry interaction contract: FAIL\n- ${failures.join('\n- ')}`);
+}
 
 process.stdout.write('mrc-apply-entry interaction contract: PASS\n');

+ 1 - 1
docs/plans/2026-09-16-mrc-mobile-visual-remediation.md

@@ -90,7 +90,7 @@ Expected: PASS。
 
 **Step 1: Write failing interaction tests**
 
-覆盖:默认患者状态渲染;点击“我要申请复印”进入身份验证;点击“我的申请”进入列表;点击患者打开/关闭本地底部面板并切换匿名患者;点击演示模式打开/关闭场景面板、切换场景后摘要更新;无患者时按 `09-添加就诊人页.html` 进入本地 Mock 添加页、完成行内校验、生成匿名患者并返回入口回填。新增 `mrc-apply-entry-interaction.test.js` 必须将唯一原始姓名/证件号/手机号输入传给匿名患者创建流程,并断言 Adapter 快照不含任一原始输入,仅含生成后的“演示就诊人”;同时静态拒绝 `patient-create` 页面和 Adapter 使用 `wx.request`、上传/选文件、Storage、既有就诊人服务或 `pages/mine/patientEdit`。
+覆盖:默认患者状态渲染;点击“我要申请复印”进入身份验证;点击“我的申请”进入列表;点击患者打开/关闭本地底部面板并切换匿名患者;点击演示模式打开/关闭场景面板、切换场景后摘要更新;无患者时按 `09-添加就诊人页.html` 进入本地 Mock 添加页、完成行内校验、生成匿名患者并返回入口回填。新增 `mrc-apply-entry-interaction.test.js` 必须以最小 `Page`/`wx` mock 加载申请入口 JS,按 WXML 绑定实际调用服务 CTA、我的申请、Demo 面板、就诊人抽屉及无患者添加动作,并断言状态变化与导航目标;不得以静态字符串检查替代行为断言。测试还必须将唯一原始姓名/证件号/手机号输入传给匿名患者创建流程,并断言 Adapter 快照不含任一原始输入,仅含生成后的“演示就诊人”;同时静态拒绝 `patient-create` 页面和 Adapter 使用 `wx.request`、上传/选文件、Storage、既有就诊人服务或 `pages/mine/patientEdit`。
 
 **Step 2: Run tests to verify RED**
 

+ 1 - 1
openspec/changes/mrc-demo-mp-apply-001/proposal.md

@@ -8,7 +8,7 @@
 - 通过小程序 Demo Adapter 读取本端快照、切换/重置场景,并以 V1 枚举创建脱敏演示申请。
 - 在 `packageMRC/pages/patient-create/index` 提供本地匿名添加就诊人闭环,以 `09-添加就诊人页.html` 为交互基线;完成后仅由 Adapter 生成匿名患者并回填申请入口。
 - 我的申请页展示 V1 支持的状态、身份、领取方式、模拟结算说明和时间轴摘要;提供至后续申请步骤或详情/进度页面的本地 Demo 路由意图。
-- 不读取真实宿主就诊人,不调用真实新增就诊人、地址、上传、支付、退款、物流、通知、取消、驳回接口,也不改动宿主首页宫格。
+- 不读取真实宿主就诊人,不调用真实新增就诊人、地址、上传、支付、退款、物流、通知、取消、驳回接口。允许仅修正既有“病案复印(Demo)”单一宿主菜单的目标路由,并归一该同菜单产生的重复项;其他宿主菜单及宿主网络能力保持不变。
 
 ## Capabilities