466 lines
16 KiB
JavaScript
466 lines
16 KiB
JavaScript
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)
|
|
})
|