Sfoglia il codice sorgente

test: 收紧病案复印匿名交互边界

anubis 1 giorno fa
parent
commit
86e88d0e48

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

@@ -21,9 +21,9 @@ function readOptional(filePath) {
 }
 
 function assertNoForbiddenCapability(source, scope) {
-  expect(!/\bwx\.request\b/.test(source), `${scope}不得使用 wx.request 网络能力`);
+  expect(!/\bwx\.(?:request|downloadFile|connectSocket)\b|\bwx\.cloud\.callFunction\b/.test(source), `${scope}不得使用请求、下载、长连接或云函数网络能力`);
   expect(!/\bwx\.\w*Storage\w*\b/i.test(source), `${scope}不得使用任意 wx.*Storage*(包括 getStorageInfo)能力`);
-  expect(!/\bwx\.(?:upload\w*|choose(?:Image|Video|Media|MessageFile)|openDocument|saveImageToPhotosAlbum)\b/.test(source), `${scope}不得使用上传、选择图片/视频/文件等能力`);
+  expect(!/\bwx\.(?:upload\w*|choose(?:Image|Media|Video|MessageFile|Address|Location|Invoice|Avatar)|getLocation|openDocument|saveImageToPhotosAlbum)\b/.test(source), `${scope}不得使用上传、选择图片/视频/文件/地址/位置/发票/头像等能力`);
   expect(!/\bconsole\.\w+\b/.test(source), `${scope}不得写入 console,避免泄漏本地输入`);
   expect(!/(?:utils\/(?:patientMaintenance|patientSelector|homePatient)|pages\/mine\/patientEdit)/.test(source), `${scope}不得引用既有就诊人服务或页面`);
 }
@@ -119,6 +119,8 @@ const adapterSource = readOptional(adapterPath);
 expect(Boolean(applyEntryTemplate), '申请入口 WXML 必须存在');
 expect(Boolean(patientCreateTemplate), '本地 Mock 添加就诊人页必须提供可操作表单');
 expect(Boolean(patientCreateScript), '本地 Mock 添加就诊人页必须存在,当前交互契约应保持 RED 直至该页实现');
+expect(!/setPatientsForTest/.test(`${applyEntryTemplate}\n${readOptional(applyEntryScriptPath)}`), '申请入口 UI 源码不得引用仅测试可用的 setPatientsForTest');
+expect(!/setPatientsForTest/.test(`${patientCreateTemplate}\n${patientCreateScript}`), '添加就诊人 UI 源码不得引用仅测试可用的 setPatientsForTest');
 
 const serviceHandler = tagBindingForClass(applyEntryTemplate, 'service-btn', '服务 CTA');
 const applicationHandler = tagBindingForClass(applyEntryTemplate, 'my-application-card', '我的申请');
@@ -158,28 +160,40 @@ try {
   const applyDefinition = loadPage(applyEntryScriptPath, '申请入口');
   const applyPage = applyDefinition && mountPage(applyDefinition);
   if (applyPage && typeof demoState.setPatientsForTest === 'function') {
-    demoState.setPatientsForTest({ patients: [], currentPatientId: '' });
+    let unsafeFixtureRejected = false;
+    try {
+      const unsafeFixture = demoState.setPatientsForTest({
+        patients: [{ id: 'unsafe-patient', displayName: 'PATIENT_INPUT_NAME_MUST_NOT_PERSIST', mobile: '13800138000' }],
+        currentPatientId: 'unsafe-patient'
+      });
+      unsafeFixtureRejected = Boolean(unsafeFixture && unsafeFixture.ok === false);
+    } catch (error) {
+      unsafeFixtureRejected = true;
+    }
+    expect(unsafeFixtureRejected, 'setPatientsForTest 必须拒绝含患者原始信息的任意对象参数');
+
+    const emptyFixture = demoState.setPatientsForTest('EMPTY');
+    expect(emptyFixture && emptyFixture.ok === true, 'setPatientsForTest 只能接受固定匿名 EMPTY fixture');
     invoke(applyPage, 'onShow', {}, '无就诊人入口刷新');
     invoke(applyPage, serviceHandler, {}, '无就诊人服务 CTA');
     expect(applyPage.data.showNoPatientGuide === true, '无就诊人点击“我要申请复印”必须展示本地添加引导/弹层');
     invoke(applyPage, patientAddHandler, {}, '无就诊人添加');
     expect(navigations.some((item) => item.url === '/packageMRC/pages/patient-create/index'), '无就诊人添加必须实际导航到本地 patient-create 页面');
 
-    demoState.setPatientsForTest({
-      patients: [
-        { id: 'test-patient-a', displayName: '演示就诊人 A', tag: '默认' },
-        { id: 'test-patient-b', displayName: '演示就诊人 B', tag: '本人' }
-      ],
-      currentPatientId: 'test-patient-a'
-    });
+    const twoAnonymousFixture = demoState.setPatientsForTest('TWO_ANONYMOUS');
+    expect(twoAnonymousFixture && twoAnonymousFixture.ok === true, 'setPatientsForTest 只能接受固定匿名 TWO_ANONYMOUS fixture');
     invoke(applyPage, 'onShow', {}, '有就诊人入口刷新');
-    expect((applyPage.data.currentPatient || {}).id === 'test-patient-a', '入口 onShow 必须从本地 Adapter 回填当前就诊人');
+    expect(applyPage.data.currentPatient && /演示就诊人/.test(applyPage.data.currentPatient.displayName || ''), '入口 onShow 必须从本地 Adapter 回填匿名当前就诊人');
 
     invoke(applyPage, patientSelectorHandler, {}, '就诊人选择');
     expect(applyPage.data.showPatientSheet === true, '点击当前就诊人必须打开底部选择抽屉');
-    invoke(applyPage, patientSheetHandler, { currentTarget: { dataset: { id: 'test-patient-b' } } }, '就诊人抽屉选项');
-    expect(applyPage.data.showPatientSheet === false, '选择就诊人后必须关闭底部抽屉');
-    expect((applyPage.data.currentPatient || {}).id === 'test-patient-b', '选择就诊人后必须回填当前就诊人卡片');
+    const alternativePatient = (applyPage.data.patients || []).find((patient) => patient.id !== (applyPage.data.currentPatient || {}).id);
+    expect(Boolean(alternativePatient), 'TWO_ANONYMOUS fixture 必须提供两名匿名就诊人');
+    if (alternativePatient) {
+      invoke(applyPage, patientSheetHandler, { currentTarget: { dataset: { id: alternativePatient.id } } }, '就诊人抽屉选项');
+      expect(applyPage.data.showPatientSheet === false, '选择就诊人后必须关闭底部抽屉');
+      expect((applyPage.data.currentPatient || {}).id === alternativePatient.id, '选择就诊人后必须回填当前就诊人卡片');
+    }
 
     invoke(applyPage, demoEntryHandler, {}, 'Demo 模式入口');
     expect(applyPage.data.showDemoMode === true, '点击 Demo 模式入口必须打开辅助面板');
@@ -199,9 +213,9 @@ try {
     const originalCreate = demoState.createAnonymousPatient;
     let createCalls = [];
     if (typeof originalCreate === 'function') {
-      demoState.createAnonymousPatient = (input) => {
-        createCalls.push(input);
-        return originalCreate(input);
+      demoState.createAnonymousPatient = (...args) => {
+        createCalls.push(args);
+        return originalCreate.apply(demoState, args);
       };
     } else {
       failures.push('Demo Adapter 必须提供 createAnonymousPatient,且不能写入原始表单值');
@@ -219,6 +233,7 @@ try {
         invoke(patientPage, patientAgreementHandler, {}, '隐私协议确认');
         invoke(patientPage, patientSubmitHandler, {}, '有效表单提交');
         expect(createCalls.length === 1, '有效表单提交必须恰好调用一次 createAnonymousPatient');
+        expect(createCalls.length === 1 && createCalls[0].length === 0, 'patient-create 调用 createAnonymousPatient 时不得传入姓名、证件号、手机号或任意表单 payload');
         expect(backs.length > 0, '匿名就诊人创建成功后必须 navigateBack 返回申请入口');
         invoke(applyPage, 'onShow', {}, 'patient-create 返回后的入口刷新');
         expect(applyPage.data.currentPatient && /演示就诊人/.test(applyPage.data.currentPatient.displayName || ''), 'patient-create 返回后入口 onShow 必须回填匿名就诊人');
@@ -235,7 +250,7 @@ if (typeof demoState.getSnapshot === 'function') {
   const snapshotResult = demoState.getSnapshot();
   expect(snapshotResult && snapshotResult.ok, '匿名患者创建后必须能读取 Demo Adapter 快照');
   const snapshotText = JSON.stringify(snapshotResult && snapshotResult.data);
-  ['测试姓名', '110101199001011234', '13800138000'].forEach((input) => {
+  ['PATIENT_INPUT_NAME_MUST_NOT_PERSIST', '测试姓名', '110101199001011234', '13800138000'].forEach((input) => {
     expect(!snapshotText.includes(input), `Adapter 快照不得包含 patient-create 的原始输入:${input}`);
   });
 }

+ 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` 必须以最小 `Page`/`wx` mock 加载申请入口和 `patient-create` JS,按 WXML 绑定实际调用服务 CTA、我的申请、Demo 面板/场景芯片、就诊人抽屉、无患者添加、表单输入/校验/提交和底部首页动作,并断言状态变化与导航目标;不得以静态字符串检查替代行为断言。Adapter 必须提供仅测试可用的受控患者状态辅助方法,以隔离无患者与多患者场景且不影响用户可见 Demo 行为。测试还必须将唯一原始姓名/证件号/手机号输入传给匿名患者创建流程,并断言 Adapter 快照不含任一原始输入,仅含生成后的“演示就诊人”;同时静态拒绝申请入口、`patient-create` 页面和 Adapter 使用任何 `wx.*Storage*`(含 `getStorageInfo`)、上传、选择图片/视频/文件、`console.*`、既有就诊人服务或 `pages/mine/patientEdit`。
+覆盖:默认患者状态渲染;点击“我要申请复印”进入身份验证;点击“我的申请”进入列表;点击患者打开/关闭本地底部面板并切换匿名患者;点击演示模式打开/关闭场景面板、切换场景后摘要更新;无患者时按 `09-添加就诊人页.html` 进入本地 Mock 添加页、完成行内校验、生成匿名患者并返回入口回填。新增 `mrc-apply-entry-interaction.test.js` 必须以最小 `Page`/`wx` mock 加载申请入口和 `patient-create` JS,按 WXML 绑定实际调用服务 CTA、我的申请、Demo 面板/场景芯片、就诊人抽屉、无患者添加、表单输入/校验/提交和底部首页动作,并断言状态变化与导航目标;不得以静态字符串检查替代行为断言。Adapter 必须提供仅测试可用的受控患者状态辅助方法,且仅接受固定匿名 `EMPTY`/`TWO_ANONYMOUS` fixture、拒绝对象参数,申请入口与添加就诊人 UI 不得引用它。测试会输入姓名/证件号/手机号以验证本页校验,但提交时 `createAnonymousPatient` 必须无参数调用,并断言 Adapter 快照不含任一原始输入,仅含生成后的“演示就诊人”;同时静态拒绝申请入口、`patient-create` 页面和 Adapter 使用请求、下载、长连接、云函数、任意 `wx.*Storage*`(含 `getStorageInfo`)、上传、选择图片/视频/文件/地址/位置/发票/头像、`console.*`、既有就诊人服务或 `pages/mine/patientEdit`。
 
 **Step 2: Run tests to verify RED**
 

+ 2 - 2
docs/visual-baselines/mrc-mobile-apply-entry.md

@@ -21,7 +21,7 @@
 | 无就诊人引导与添加流程 | `.patient-add-card`、`bindtap="openPatientCreate"` | 无患者场景 | 点击“添加就诊人”导航至 `/packageMRC/pages/patient-create/index`;该页本地完成模拟填写/校验后创建 `演示就诊人 N`,返回入口并回填为当前就诊人 | 无真实实名认证、账号、OCR、资料写入或网络 | 空态以虚线卡替代选择器;CTA 必须可点击并实际离开入口页,不得停留在“不可用”弹层 |
 | 就诊人底部选择面板 | `.patient-sheet`、`.sheet-item`、选中态、取消 | 两个匿名演示就诊人,性别年龄仅可为演示占位 | 点击遮罩/取消关闭;点击列表项选中并回填主卡 | 无编辑、删除、真实人员查询 | 底部圆角抽屉、标题“选择就诊人”、单选标记与安全区完整;不以页面跳转替代抽屉 |
 | 温馨提示卡 | `.notice-card`、提示列表 | 静态合规提示;不得含真实收费/个人信息 | 无主操作;长文可随页面滚动 | 不承诺真实时效、支付、快递或院内数据 | 暖色浅底、橙色描边/图标、标题及三条提示可读,位于就诊人区块之后 |
-| 原型底部导航语义 | `.bottom-nav`、首页/账单/消息/我的 | 本地静态宿主导航状态 | “首页”返回宿主首页;其余项使用既有宿主可达路由,缺失时提示不可用且不伪造跳转 | 不新增宿主业务页、不改已有 TabBar 配置 | 固定底部白色导航、四项图标/文案、首页高亮,内容不被安全区遮挡 |
+| 原型底部导航语义 | `.bottom-nav`、首页/账单/消息/我的 | 本地静态宿主导航状态 | “首页”仅以 `wx.switchTab` 返回既有宿主首页;其余项使用既有宿主可达路由,缺失时提示不可用且不伪造跳转 | 不新增宿主业务页、不改已有 TabBar 配置、不调用网络或其他宿主业务能力 | 固定底部白色导航、四项图标/文案、首页高亮,内容不被安全区遮挡 |
 
 ## 本地 Mock 添加就诊人页契约
 
@@ -33,7 +33,7 @@
 | 姓名、证件类型、证件号、手机号、验证码表单 | 与原型一致的表单行、选择器、验证码/下一步按钮 | 输入、选择、行内校验、焦点/禁用/错误态均真实可操作;“获取验证码”只反馈本地演示验证码状态 | 不调用短信、实名认证、OCR 或服务端校验;提交值不展示或持久化为 PII |
 | 本地 Mock 完成与回填 | 提交成功提示、返回申请入口 | 通过本地 Adapter 生成 `演示就诊人 N` 和非敏感演示标识;`navigateBack` 后入口 `onShow` 读取 Adapter 并显示为当前就诊人 | 仅内存状态;不写 Storage、文件或网络;不得保留用户输入的姓名/证件/手机号 | 成功提示后返回入口;当前就诊人卡出现匿名名称和默认/已选标签 |
 
-本页可保留原型字段和校验反馈以验证未来上线时的操作体验,但 Demo 提交只能写入 Adapter 生成的匿名患者。任何手输个人信息均不得回显、写入日志、Storage 或 Mock 固定数据。
+本页可保留原型字段和校验反馈以验证未来上线时的操作体验,但 Demo 提交只能调用无参数的 Adapter 匿名创建动作,不能把任一表单 payload 传入 Adapter。任何手输个人信息均不得回显、写入日志、Storage 或 Mock 固定数据。
 
 ## 交互与路由验收规则
 

+ 8 - 2
openspec/changes/mrc-demo-mp-apply-001/design.md

@@ -2,6 +2,8 @@
 
 移动端原型确认了申请入口与我的申请的视觉和主要路径,但历史模块材料同时含有未授权的真实宿主能力及过多生产状态。本期 Demo 只验证小程序本地 Mock 场景,须将页面行为收敛到 V1。
 
+本整改变更仅以本目录的当前 Proposal、Design、Tasks、Spec 和已评审原型作为授权与验收依据;历史旧规格中涉及真实宿主患者、接口、支付或扩展状态的表述不构成本轮授权或验收依据,无需改写历史文档。
+
 ## Goals / Non-Goals
 
 **Goals:**
@@ -13,7 +15,7 @@
 
 **Non-Goals:**
 
-- 不读取真实宿主就诊人、地址、登录或订单接口;不调用真实新增就诊人、支付、退款、物流、通知或宿主首页能力。
+- 不读取真实宿主就诊人、地址、登录或订单接口;不调用真实新增就诊人、支付、退款、物流、通知或其他宿主业务能力。仅允许本地底部导航通过 `wx.switchTab` 返回既有宿主首页,不调用网络能力。
 - 不实现材料上传、身份验证、复印内容配置、提交表单、详情/支付完整页面;它们分别由后续 Demo 模块实现。
 - 不实现 V1 外的待补缴、待支付、驳回、取消、真实筛选统计或行内提示业务规则。
 
@@ -21,7 +23,11 @@
 
 ### 本地匿名添加就诊人取代真实宿主能力
 
-入口页仅经 V1 Adapter 展示匿名 `applicant` / `patient`,不得读取既有宿主就诊人,也不得调用真实新增就诊人接口。无就诊人 CTA 进入新增 `packageMRC/pages/patient-create/index`:该页按原型完成本地输入、校验和提交反馈,但 Adapter 只生成 `演示就诊人 N` 与非敏感标识;返回入口后由 `onShow` 回填。页面不得暗示真实实名认证、短信验证或身份验证已完成。
+入口页仅经 V1 Adapter 展示匿名 `applicant` / `patient`,不得读取既有宿主就诊人,也不得调用真实新增就诊人接口。无就诊人 CTA 进入新增 `packageMRC/pages/patient-create/index`:该页按原型完成本地输入、校验和提交反馈,但提交时不得向 Adapter 传入任何表单 payload,Adapter 只生成 `演示就诊人 N` 与非敏感标识;返回入口后由 `onShow` 回填。页面不得暗示真实实名认证、短信验证或身份验证已完成。
+
+### 受控测试状态不进入患者 UI
+
+Adapter 可导出 `setPatientsForTest`,但它只接受固定匿名 fixture 标识 `EMPTY` 与 `TWO_ANONYMOUS`,拒绝对象和任何含患者原始信息的参数;该 API 仅用于自动化测试隔离场景。申请入口与添加就诊人页面的 WXML/JS 均不得引用它,患者 UI 只读取普通匿名快照。
 
 ### 场景芯片仅属于演示模式面板
 

+ 1 - 1
openspec/changes/mrc-demo-mp-apply-001/specs/mrc-demo-mp-application-hub/spec.md

@@ -20,7 +20,7 @@
 
 系统 MUST 在无就诊人状态把患者引导到 `packageMRC/pages/patient-create/index`,并以 `09-添加就诊人页.html` 的表单、校验、返回与首页交互语义实现本地 Mock 流程。完成后系统 MUST 仅通过小程序本地 Adapter 生成匿名演示患者、返回申请入口并回填当前就诊人。
 
-系统 MUST NOT 读取真实宿主就诊人,MUST NOT 调用真实新增就诊人、实名认证、短信、OCR 或生产接口,也不得把输入的个人信息写入 Adapter 快照、Storage、日志或固定 Mock 数据。Adapter 只可返回生成后的匿名演示就诊人。`patient-create` 页面与 MRC Demo Adapter MUST NOT 使用 `wx.request`、上传/选文件能力、任何 Storage API,或引用既有就诊人服务(`patientMaintenance`、`patientSelector`、`homePatient`)和 `pages/mine/patientEdit`。
+系统 MUST NOT 读取真实宿主就诊人,MUST NOT 调用真实新增就诊人、实名认证、短信、OCR 或生产接口,也不得把输入的个人信息写入 Adapter 快照、Storage、日志或固定 Mock 数据。`patient-create` 提交 MUST NOT 将任何表单 payload 传给 Adapter;Adapter 只可无参数生成匿名演示就诊人。`patient-create` 页面与 MRC Demo Adapter MUST NOT 使用 `wx.request`、`wx.downloadFile`、`wx.connectSocket`、`wx.cloud.callFunction`、上传/选择图片视频文件地址位置发票头像能力、任何 Storage API 或 `console.*`,也不得引用既有就诊人服务(`patientMaintenance`、`patientSelector`、`homePatient`)和 `pages/mine/patientEdit`。
 
 #### Scenario: 从无就诊人状态添加匿名患者
 - **WHEN** 演示人员在申请入口点击“添加就诊人”,完成本地表单校验并提交