const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const vm = require('node:vm') const { createRequire } = require('node:module') const vue = require('vue') const { parse, compileScript } = require('vue/compiler-sfc') const esbuild = createRequire(require.resolve('vite'))('esbuild') const Schema = createRequire(require.resolve('element-plus'))('async-validator').default const privacyModule = { exports: {} } vm.runInNewContext( esbuild.transformSync( fs.readFileSync(path.join(__dirname, '../src/utils/healthPrivacy.ts'), 'utf8'), { loader: 'ts', format: 'cjs' } ).code, { module: privacyModule, exports: privacyModule.exports } ) const flowModule = { exports: {} } vm.runInNewContext( esbuild.transformSync( fs.readFileSync(path.join(__dirname, '../src/views/health/identity/collectionFlow.ts'), 'utf8'), { loader: 'ts', format: 'cjs' } ).code, { module: flowModule, exports: flowModule.exports } ) const formPath = path.join(__dirname, '../src/views/health/identity/IdentityForm.vue') const { descriptor } = parse(fs.readFileSync(formPath, 'utf8'), { filename: formPath }) const script = compileScript(descriptor, { id: 'collection-wizard-test' }) const compiled = esbuild.transformSync(script.content, { loader: 'ts', format: 'cjs' }).code const identityModule = { exports: {} } vm.runInNewContext( esbuild.transformSync(fs.readFileSync(path.join(__dirname, '../src/utils/idCard.ts'), 'utf8'), { loader: 'ts', format: 'cjs' }).code, { module: identityModule, exports: identityModule.exports } ) // 执行实际 Vue 表单逻辑,用真实 async-validator 校验规则,仅替换 HTTP 与渲染组件。 function fixture({ identity, draft } = {}) { const requests = [] const warnings = [] const draftTransport = { saveError: null } const api = { identityTypes: [], genderOptions: [], ethnicities: [], residenceTypeOptions: [], livingStatusOptions: [], householdTypeOptions: [], educationLevelOptions: [], getIdentityAreaOptions: async () => [], getIdentity: async () => ({ ...identity }), createIdentity: async (payload) => { requests.push({ method: 'create', payload: { ...payload } }) return 123 }, updateIdentity: async (payload) => requests.push({ method: 'update', payload: { ...payload } }) } const module = { exports: {} } vm.runInNewContext(compiled, { module, exports: module.exports, RegExp, require: (id) => { if (id === 'vue') return vue if (id === '@/api/health') return api if (id === '@/utils/idCard') return identityModule.exports if (id === '@/utils/healthPrivacy') return privacyModule.exports if (id === '@/hooks/web/useMessage') return { useMessage: () => ({ warning: (value) => warnings.push(value), success: () => {} }) } if (id === './useCollectionDraft') return { useCollectionDraft: (data, accountMode, activeStep, _visible, readOnly) => ({ draftMessage: vue.ref('填写过程中会自动保存草稿,密码需要重新输入。'), draftSaving: vue.ref(false), draftFailed: vue.ref(false), initializeDraft: async () => { if (draft && !readOnly.value) { data.value = { ...data.value, ...draft.fields, password: undefined } accountMode.value = draft.fields.accountMode || 'new' activeStep.value = draft.currentStep } }, saveDraftNow: async () => { if (draftTransport.saveError) throw draftTransport.saveError }, pauseDraft: async () => {}, resumeDraft: () => {} }) } if ( [ './SocialSupport.vue', './CollectionHealthSteps.vue', './CollectionFamilySteps.vue' ].includes(id) ) return {} if (id === './collectionFlow') return flowModule.exports throw new Error(`Unexpected import: ${id}`) } }) const scope = vue.effectScope() const form = scope.run(() => module.exports.default.setup({}, { expose: () => {}, emit: () => {} }) ) const validateFields = async (fields) => { const keys = fields || Object.keys(form.rules.value) const errors = {} await Promise.all( keys.map(async (key) => { const rules = (form.rules.value[key] || []).map(({ trigger: _trigger, ...rule }) => rule) const value = key.split('.').reduce((data, part) => data?.[part], form.data.value) try { await new Schema({ [key]: rules }).validate({ [key]: value }) } catch (error) { Object.assign(errors, error.fields) } }) ) if (Object.keys(errors).length) throw errors return true } form.formRef.value = { validateField: validateFields, validate: () => validateFields(), clearValidate: () => {} } return { form, api, requests, warnings, draftTransport, stop: () => scope.stop() } } const basicInfo = { realName: '测试老人', idCard: '11010519491231002X', gender: 2, birthDate: '1949-12-31', ethnicity: '汉族', mobile: '13800000001', password: 'test-only', consentConfirmed: true } const socialInfo = { residenceArea: '110105', residenceAddress: ' 测试地址一号 ', educationLevel: 1, maritalStatus: 0, elderlyUniversity: false } test('a new form starts on step one and incomplete required fields block advancing', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') assert.equal(f.form.activeStep.value, 1) await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 1) assert.equal(f.form.showSocialInfo.value, false) assert.equal(f.requests.length, 0) assert.equal(f.warnings.length, 1) }) test('valid basic information advances without requiring unfilled step two or creating an account', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, { gender: 3, birthDate: undefined }) await f.form.handleIdCardInput('11010519491231002x') await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 2) assert.equal(f.form.showBasicInfo.value, false) assert.equal(f.form.showSocialInfo.value, true) assert.equal(f.form.data.value.birthDate, '1949-12-31') assert.equal(f.form.data.value.gender, 2) assert.equal(f.requests.length, 0) }) test('returning preserves inputs and checks required information again before advancing', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo) await f.form.selectStep(2) Object.assign(f.form.data.value, socialInfo) await f.form.selectStep(1) assert.equal(f.form.data.value.educationLevel, 1) f.form.data.value.realName = ' ' await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 1) f.form.data.value.realName = '更正姓名' await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 2) assert.equal(f.form.data.value.residenceAddress, socialInfo.residenceAddress) }) test('calling submit from step one cannot bypass validation or create a partial formal record', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') await f.form.submit() assert.equal(f.form.activeStep.value, 1) Object.assign(f.form.data.value, basicInfo) await f.form.submit() assert.equal(f.form.activeStep.value, 2) assert.equal(f.requests.length, 0) }) const contact = { name: '测试联系人', relationship: 'child', phone: '13900000001', primary: true, contactOnly: true } const goToStep = async (form, step) => { while (form.activeStep.value < step) { const before = form.activeStep.value await form.selectStep(before + 1) if (form.activeStep.value === before) break } } test('seven sequential steps require address, education, care and contact before one complete save', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo) await f.form.selectStep(2) await f.form.selectStep(3) assert.equal(f.form.activeStep.value, 2) assert.equal(f.requests.length, 0) Object.assign(f.form.data.value, socialInfo) await goToStep(f.form, 4) await f.form.selectStep(5) assert.equal(f.form.activeStep.value, 4) f.form.data.value.careLevel = 1 await goToStep(f.form, 7) await f.form.submit() assert.equal(f.requests.length, 0) f.form.data.value.emergencyContacts = [{ ...contact }] await f.form.submit() assert.equal(f.requests.length, 1) const saved = f.requests[0].payload assert.equal(saved.collectionStep, 7) assert.equal(saved.idCard, basicInfo.idCard) assert.equal(saved.residenceAddress, '测试地址一号') assert.equal(saved.maritalStatus, 0) assert.equal(saved.elderlyUniversity, false) assert.equal(saved.occupation, null) assert.equal(saved.disabled, false) assert.equal(saved.consentConfirmed, true) assert.equal(f.form.visible.value, false) assert.equal(f.form.data.value.password, undefined) }) test('a draft on step two without the unsaved new-account password returns to step one', async (t) => { const f = fixture({ draft: { currentStep: 2, fields: { ...basicInfo, ...socialInfo } } }) t.after(f.stop) await f.form.open('create') assert.equal(f.form.activeStep.value, 1) assert.equal(f.form.data.value.educationLevel, 1) f.form.data.value.password = 're-entered' await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 2) }) test('a valid existing-account draft resumes step two while a normal edit starts on step one', async (t) => { const identity = { ...basicInfo, password: undefined, id: 456, identityType: 10, memberId: 8, collectionStep: 2 } const normal = fixture({ identity }) const resumed = fixture({ identity, draft: { currentStep: 2, fields: socialInfo } }) t.after(normal.stop) t.after(resumed.stop) await normal.form.open('update', 456) assert.equal(normal.form.activeStep.value, 1) await resumed.form.open('update', 456) assert.equal(resumed.form.activeStep.value, 2) }) test('read-only older records can be reviewed without validation or writes', async (t) => { const f = fixture({ identity: { id: 456, identityType: 10, realName: '旧档案', collectionStep: 1 } }) t.after(f.stop) await f.form.open('view', 456) await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 2) await f.form.submit() assert.equal(f.requests.length, 0) }) test('underage dates block advancement and unavailable steps cannot be selected', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, { birthDate: '2020-01-01' }) await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 1) await f.form.selectStep(3) assert.equal(f.form.activeStep.value, 1) }) test('direct jumping cannot bypass sequential validation and final submit routes errors to the owning step', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, socialInfo, { careLevel: 1, emergencyContacts: [{ ...contact }] }) await f.form.selectStep(7) assert.equal(f.form.activeStep.value, 1) await goToStep(f.form, 7) f.form.data.value.educationLevel = null await f.form.submit() assert.equal(f.form.activeStep.value, 2) assert.equal(f.requests.length, 0) }) test('disabled elderly require a type and duplicate contact phones or priority flags cannot save', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, socialInfo, { careLevel: 3, disabled: true }) await goToStep(f.form, 4) await f.form.selectStep(5) assert.equal(f.form.activeStep.value, 4) f.form.data.value.disabilityType = 'physical' await goToStep(f.form, 7) f.form.data.value.emergencyContacts = [{ ...contact }, { ...contact, primary: false }] await f.form.submit() assert.equal(f.requests.length, 0) f.form.data.value.emergencyContacts[1].phone = '13900000002' f.form.data.value.emergencyContacts[1].primary = true await f.form.submit() assert.equal(f.requests.length, 0) f.form.data.value.emergencyContacts[1].primary = false await f.form.submit() assert.equal(f.requests.length, 1) }) test('a failed create retains the password and all input for retry', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, socialInfo, { careLevel: 1, emergencyContacts: [{ ...contact }] }) await goToStep(f.form, 7) let fail = true f.api.createIdentity = async (payload) => { if (fail) throw new Error('retry-required') f.requests.push({ method: 'create', payload }) return 123 } await assert.rejects(f.form.submit(), /retry-required/) assert.equal(f.form.data.value.password, basicInfo.password) assert.equal(f.form.visible.value, true) assert.equal(f.form.activeStep.value, 7) fail = false await f.form.submit() assert.equal(f.requests.length, 1) }) test('complete draft resumes step seven only after all prior required steps are valid', async (t) => { const f = fixture({ identity: { ...basicInfo, password: undefined, id: 456, memberId: 8, identityType: 10 }, draft: { currentStep: 7, fields: { ...socialInfo, careLevel: 1, emergencyContacts: [{ ...contact }] } } }) t.after(f.stop) await f.form.open('update', 456) assert.equal(f.form.activeStep.value, 7) const invalid = fixture({ identity: { ...basicInfo, password: undefined, id: 456, memberId: 8, identityType: 10 }, draft: { currentStep: 7, fields: socialInfo } }) t.after(invalid.stop) await invalid.form.open('update', 456) assert.equal(invalid.form.activeStep.value, 4) }) test('malformed restored birthdays and future diagnoses fail before advancing', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, basicInfo, socialInfo, { careLevel: 1, birthDate: '1949-02-31' }) await f.form.selectStep(2) assert.equal(f.form.activeStep.value, 1) f.form.data.value.birthDate = basicInfo.birthDate await goToStep(f.form, 5) f.form.data.value.chronicDiseaseStatus = 1 f.form.data.value.chronicDiseases = [{ code: 'diabetes', diagnosedDate: '2999-01-01' }] await f.form.selectStep(6) assert.equal(f.form.activeStep.value, 5) }) test('volunteer and family profiles save in one form without elderly collection fields', async (t) => { for (const identityType of [20, 30]) { const f = fixture() t.after(f.stop) await f.form.open('create') Object.assign(f.form.data.value, { identityType, realName: '测试档案', gender: 1, mobile: '13800000001', password: 'test-only', skills: '陪伴交流', serviceAreas: '测试社区', idCard: 'elder-only', careLevel: 1, insuranceType: 1, consentConfirmed: true }) assert.equal(f.form.isElder.value, false) assert.equal( f.form.profilePresentation.value.title, identityType === 20 ? '志愿者资料登记' : '家属资料登记' ) await f.form.submit() assert.equal(f.requests.length, 1) const payload = f.requests[0].payload assert.equal(payload.identityType, identityType) assert.equal(payload.idCard, undefined) assert.equal(payload.careLevel, undefined) assert.equal(payload.insuranceType, undefined) assert.equal(payload.consentConfirmed, undefined) assert.equal(payload.skills, '陪伴交流') assert.equal(f.form.visible.value, false) } }) test('closing after a failed draft save preserves the form and supports a retry', async (t) => { const f = fixture() t.after(f.stop) await f.form.open('create') f.form.data.value.realName = '尚未保存的输入' f.draftTransport.saveError = new Error('offline') let closed = false await f.form.beforeClose(() => { closed = true }) assert.equal(closed, false) assert.equal(f.form.data.value.realName, '尚未保存的输入') assert.equal(f.warnings.length, 1) f.draftTransport.saveError = null await f.form.beforeClose(() => { closed = true }) assert.equal(closed, true) })