|
|
@@ -3,9 +3,9 @@ const path = require('path');
|
|
|
|
|
|
const root = path.join(__dirname, '..');
|
|
|
const applyEntryDir = path.join(root, 'packageMRC', 'pages', 'apply-entry');
|
|
|
+const patientCreateDir = path.join(root, 'packageMRC', 'pages', 'patient-create');
|
|
|
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');
|
|
|
@@ -21,32 +21,69 @@ function readOptional(filePath) {
|
|
|
}
|
|
|
|
|
|
function assertNoForbiddenCapability(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(!/\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(!/\bconsole\.\w+\b/.test(source), `${scope}不得写入 console,避免泄漏本地输入`);
|
|
|
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);
|
|
|
+function classNames(attributes) {
|
|
|
+ const match = attributes.match(/\bclass\s*=\s*(["'])(.*?)\1/);
|
|
|
+ return match ? match[2].trim().split(/\s+/).filter(Boolean) : [];
|
|
|
+}
|
|
|
+
|
|
|
+function findViewViolations(wxml, childClass, requiredAncestorClass) {
|
|
|
+ const stack = [];
|
|
|
+ const violations = [];
|
|
|
+ const tokens = /<\/?view\b[^>]*>/g;
|
|
|
+ let token;
|
|
|
+ while ((token = tokens.exec(wxml))) {
|
|
|
+ const raw = token[0];
|
|
|
+ if (/^<\//.test(raw)) {
|
|
|
+ if (stack.length) stack.pop();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const classes = classNames(raw);
|
|
|
+ if (classes.includes(childClass) && !stack.some((item) => item.includes(requiredAncestorClass))) {
|
|
|
+ violations.push(token.index);
|
|
|
+ }
|
|
|
+ if (!/\/\s*>$/.test(raw)) stack.push(classes);
|
|
|
+ }
|
|
|
+ return violations;
|
|
|
+}
|
|
|
+
|
|
|
+function tagBindingForClass(template, className, interactionName) {
|
|
|
+ const tag = (template.match(/<[^>]+>/g) || []).find((raw) => classNames(raw).includes(className));
|
|
|
expect(Boolean(tag), `${interactionName}必须保留 .${className} 可点击节点`);
|
|
|
if (!tag) return '';
|
|
|
- const binding = /bindtap=["']([\w$]+)["']/.exec(tag[0]);
|
|
|
+ const binding = /bindtap=["']([\w$]+)["']/.exec(tag);
|
|
|
expect(Boolean(binding), `${interactionName}必须绑定 tap 处理函数`);
|
|
|
return binding ? binding[1] : '';
|
|
|
}
|
|
|
|
|
|
-function loadApplyEntryPage() {
|
|
|
+function inputBindings(template) {
|
|
|
+ const bindings = [];
|
|
|
+ const tags = template.match(/<input\b[^>]*>/g) || [];
|
|
|
+ tags.forEach((tag, index) => {
|
|
|
+ const binding = /bindinput=["']([\w$]+)["']/.exec(tag);
|
|
|
+ if (binding) bindings.push({ handler: binding[1], index });
|
|
|
+ });
|
|
|
+ return bindings;
|
|
|
+}
|
|
|
+
|
|
|
+function loadPage(scriptPath, scope) {
|
|
|
let definition;
|
|
|
const originalPage = global.Page;
|
|
|
global.Page = (config) => { definition = config; };
|
|
|
- delete require.cache[require.resolve(applyEntryScriptPath)];
|
|
|
+ delete require.cache[require.resolve(scriptPath)];
|
|
|
try {
|
|
|
- require(applyEntryScriptPath);
|
|
|
+ require(scriptPath);
|
|
|
} catch (error) {
|
|
|
- failures.push(`申请入口 JS 必须可由 Page mock 加载:${error.message}`);
|
|
|
+ failures.push(`${scope} JS 必须可由 Page mock 加载:${error.message}`);
|
|
|
} finally {
|
|
|
global.Page = originalPage;
|
|
|
}
|
|
|
- expect(Boolean(definition), '申请入口必须通过 Page 注册,供真实小程序事件分发');
|
|
|
+ expect(Boolean(definition), `${scope}必须通过 Page 注册,供真实小程序事件分发`);
|
|
|
return definition;
|
|
|
}
|
|
|
|
|
|
@@ -63,7 +100,7 @@ function mountPage(definition) {
|
|
|
|
|
|
function invoke(page, handlerName, event, interactionName) {
|
|
|
if (!handlerName || typeof page[handlerName] !== 'function') {
|
|
|
- failures.push(`${interactionName}的 ${handlerName || 'tap'} 处理函数必须在申请入口 Page 中实现`);
|
|
|
+ failures.push(`${interactionName}的 ${handlerName || 'tap'} 处理函数必须在 Page 中实现`);
|
|
|
return undefined;
|
|
|
}
|
|
|
try {
|
|
|
@@ -75,87 +112,131 @@ function invoke(page, handlerName, event, interactionName) {
|
|
|
}
|
|
|
|
|
|
const applyEntryTemplate = readOptional(applyEntryTemplatePath);
|
|
|
+const patientCreateTemplate = readOptional(patientCreateTemplatePath);
|
|
|
+const patientCreateScript = readOptional(patientCreateScriptPath);
|
|
|
+const adapterSource = readOptional(adapterPath);
|
|
|
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', '无就诊人添加');
|
|
|
+expect(Boolean(patientCreateTemplate), '本地 Mock 添加就诊人页必须提供可操作表单');
|
|
|
+expect(Boolean(patientCreateScript), '本地 Mock 添加就诊人页必须存在,当前交互契约应保持 RED 直至该页实现');
|
|
|
+
|
|
|
+const serviceHandler = tagBindingForClass(applyEntryTemplate, 'service-btn', '服务 CTA');
|
|
|
+const applicationHandler = tagBindingForClass(applyEntryTemplate, 'my-application-card', '我的申请');
|
|
|
+const patientSelectorHandler = tagBindingForClass(applyEntryTemplate, 'patient-selector', '就诊人选择');
|
|
|
+const patientSheetHandler = tagBindingForClass(applyEntryTemplate, 'sheet-item', '就诊人抽屉选项');
|
|
|
+const demoEntryHandler = tagBindingForClass(applyEntryTemplate, 'demo-mode-entry', 'Demo 模式入口');
|
|
|
+const demoCloseHandler = tagBindingForClass(applyEntryTemplate, 'demo-mode-close', 'Demo 模式关闭');
|
|
|
+const patientAddHandler = tagBindingForClass(applyEntryTemplate, 'patient-add-card', '无就诊人添加');
|
|
|
+const scenarioHandler = tagBindingForClass(applyEntryTemplate, 'chip', 'Demo 场景芯片');
|
|
|
+const sheetViolations = findViewViolations(applyEntryTemplate, 'sheet-item', 'patient-sheet');
|
|
|
+expect(sheetViolations.length === 0, `每个 .sheet-item 必须位于 .patient-sheet 内,违规位置:${sheetViolations.join(', ')}`);
|
|
|
+expect(/bindtap=["']goHome["']/.test(applyEntryTemplate) && /首页/.test(applyEntryTemplate), '底部“首页”必须绑定 goHome 返回宿主首页');
|
|
|
+
|
|
|
+const patientInputBindings = inputBindings(patientCreateTemplate);
|
|
|
+expect(patientInputBindings.length >= 4, '添加就诊人页必须至少保留姓名、证件号、手机号、验证码四项 bindinput 真实表单交互');
|
|
|
+const patientSubmitHandler = tagBindingForClass(patientCreateTemplate, 'next-btn', '添加就诊人提交');
|
|
|
+const patientAgreementHandler = tagBindingForClass(patientCreateTemplate, 'agree', '隐私协议确认');
|
|
|
+
|
|
|
+assertNoForbiddenCapability(readOptional(applyEntryScriptPath), '申请入口本地 Demo');
|
|
|
+assertNoForbiddenCapability(patientCreateScript, '本地 Mock 添加就诊人页');
|
|
|
+assertNoForbiddenCapability(adapterSource, 'MRC Demo Adapter');
|
|
|
|
|
|
const originalWx = global.wx;
|
|
|
const navigations = [];
|
|
|
+const switches = [];
|
|
|
+const backs = [];
|
|
|
+const toasts = [];
|
|
|
global.wx = {
|
|
|
navigateTo(options) { navigations.push(options || {}); },
|
|
|
- showToast() {}
|
|
|
+ switchTab(options) { switches.push(options || {}); },
|
|
|
+ navigateBack(options) { backs.push(options || {}); },
|
|
|
+ showToast(options) { toasts.push(options || {}); }
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
- const definition = loadApplyEntryPage();
|
|
|
- if (definition) {
|
|
|
- const page = mountPage(definition);
|
|
|
- invoke(page, patientAddHandler, {}, '无就诊人添加');
|
|
|
+ expect(typeof demoState.setPatientsForTest === 'function', 'Demo Adapter 必须提供仅测试可用的 setPatientsForTest,以隔离无就诊人与多患者场景');
|
|
|
+ const applyDefinition = loadPage(applyEntryScriptPath, '申请入口');
|
|
|
+ const applyPage = applyDefinition && mountPage(applyDefinition);
|
|
|
+ if (applyPage && typeof demoState.setPatientsForTest === 'function') {
|
|
|
+ demoState.setPatientsForTest({ patients: [], currentPatientId: '' });
|
|
|
+ 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 页面');
|
|
|
|
|
|
- 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 || '')), '有匿名就诊人时“我要申请复印”必须进入身份验证页');
|
|
|
+ demoState.setPatientsForTest({
|
|
|
+ patients: [
|
|
|
+ { id: 'test-patient-a', displayName: '演示就诊人 A', tag: '默认' },
|
|
|
+ { id: 'test-patient-b', displayName: '演示就诊人 B', tag: '本人' }
|
|
|
+ ],
|
|
|
+ currentPatientId: 'test-patient-a'
|
|
|
+ });
|
|
|
+ invoke(applyPage, 'onShow', {}, '有就诊人入口刷新');
|
|
|
+ expect((applyPage.data.currentPatient || {}).id === 'test-patient-a', '入口 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', '选择就诊人后必须回填当前就诊人卡片');
|
|
|
+
|
|
|
+ invoke(applyPage, demoEntryHandler, {}, 'Demo 模式入口');
|
|
|
+ expect(applyPage.data.showDemoMode === true, '点击 Demo 模式入口必须打开辅助面板');
|
|
|
+ invoke(applyPage, scenarioHandler, { currentTarget: { dataset: { id: 'SCN-PENDING-AUDIT' } } }, 'Demo 场景芯片');
|
|
|
+ expect((applyPage.data.snapshot || {}).activeScenarioId === 'SCN-PENDING-AUDIT', '点击场景芯片后 activeScenarioId 必须切换');
|
|
|
+ expect((applyPage.data.applicationSummary || {}).total === 1, '切换到待审核场景后我的申请摘要必须更新为 1 条');
|
|
|
+ invoke(applyPage, demoCloseHandler, {}, 'Demo 模式关闭');
|
|
|
+ expect(applyPage.data.showDemoMode === false, '关闭 Demo 模式必须恢复患者首屏');
|
|
|
+
|
|
|
+ invoke(applyPage, serviceHandler, {}, '有就诊人服务 CTA');
|
|
|
+ expect(navigations.some((item) => /^\/packageMRC\/pages\/identity\/index(?:\?|$)/.test(item.url || '')), '有匿名就诊人时“我要申请复印”必须进入身份验证页');
|
|
|
+ invoke(applyPage, applicationHandler, {}, '我的申请');
|
|
|
+ expect(navigations.some((item) => item.url === '/packageMRC/pages/my-applications/index'), '点击“我的申请”必须进入申请列表页');
|
|
|
+ invoke(applyPage, 'goHome', {}, '底部首页');
|
|
|
+ expect(switches.some((item) => item.url === '/pages/tabBar/index/index'), '底部“首页”必须通过 switchTab 返回宿主首页');
|
|
|
+
|
|
|
+ const originalCreate = demoState.createAnonymousPatient;
|
|
|
+ let createCalls = [];
|
|
|
+ if (typeof originalCreate === 'function') {
|
|
|
+ demoState.createAnonymousPatient = (input) => {
|
|
|
+ createCalls.push(input);
|
|
|
+ return originalCreate(input);
|
|
|
+ };
|
|
|
} else {
|
|
|
failures.push('Demo Adapter 必须提供 createAnonymousPatient,且不能写入原始表单值');
|
|
|
}
|
|
|
-
|
|
|
- invoke(page, applicationHandler, {}, '我的申请');
|
|
|
- expect(navigations.some((item) => item.url === '/packageMRC/pages/my-applications/index'), '点击“我的申请”必须进入申请列表页');
|
|
|
+ try {
|
|
|
+ const patientDefinition = loadPage(patientCreateScriptPath, '添加就诊人');
|
|
|
+ if (patientDefinition) {
|
|
|
+ const patientPage = mountPage(patientDefinition);
|
|
|
+ invoke(patientPage, patientSubmitHandler, {}, '空表单提交');
|
|
|
+ expect(toasts.some((item) => /请|填写|同意/.test(item.title || '')) || Boolean(patientPage.data.errorMessage), '空表单提交必须显示行内校验或明确提示');
|
|
|
+
|
|
|
+ patientInputBindings.slice(0, 4).forEach((binding, index) => {
|
|
|
+ invoke(patientPage, binding.handler, { detail: { value: ['测试姓名', '110101199001011234', '13800138000', '123456'][index] }, currentTarget: { dataset: { field: ['name', 'idCard', 'mobile', 'verificationCode'][index] } } }, `添加就诊人第 ${index + 1} 项输入`);
|
|
|
+ });
|
|
|
+ invoke(patientPage, patientAgreementHandler, {}, '隐私协议确认');
|
|
|
+ invoke(patientPage, patientSubmitHandler, {}, '有效表单提交');
|
|
|
+ expect(createCalls.length === 1, '有效表单提交必须恰好调用一次 createAnonymousPatient');
|
|
|
+ expect(backs.length > 0, '匿名就诊人创建成功后必须 navigateBack 返回申请入口');
|
|
|
+ invoke(applyPage, 'onShow', {}, 'patient-create 返回后的入口刷新');
|
|
|
+ expect(applyPage.data.currentPatient && /演示就诊人/.test(applyPage.data.currentPatient.displayName || ''), 'patient-create 返回后入口 onShow 必须回填匿名就诊人');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (typeof originalCreate === 'function') demoState.createAnonymousPatient = originalCreate;
|
|
|
+ }
|
|
|
}
|
|
|
} 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');
|
|
|
-
|
|
|
-if (typeof demoState.createAnonymousPatient === 'function') {
|
|
|
+if (typeof demoState.getSnapshot === '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) => {
|
|
|
+ ['测试姓名', '110101199001011234', '13800138000'].forEach((input) => {
|
|
|
expect(!snapshotText.includes(input), `Adapter 快照不得包含 patient-create 的原始输入:${input}`);
|
|
|
});
|
|
|
- expect(/演示就诊人/.test(snapshotText), 'Adapter 快照只能暴露生成后的匿名演示就诊人');
|
|
|
}
|
|
|
|
|
|
if (failures.length) {
|