Files
zkxw/web-admin/scripts/test-cell-storage-form-regressions.ts
2026-10-17 11:46:53 +08:00

488 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
import ts from 'typescript'
const base = new URL('../src/views/cell-storage/', import.meta.url)
const presign = 'presign-order/PresignOrderFormDialog.vue'
const panel = 'protocol/SamplePaymentPanel.vue'
const protocol = 'protocol/ProtocolFormDialog.vue'
function source(file: string) {
const text = readFileSync(new URL(file, base), 'utf8').match(
/<script setup lang="ts">([\s\S]*?)<\/script>/
)![1]
return ts.createSourceFile(file + '.ts', text, ts.ScriptTarget.Latest, true)
}
function functions(file: string, names: string[], env: Record<string, any>) {
env = {
isEdit: { value: false },
editDetailLoaded: { value: true },
loading: { value: false },
formReady: { value: true },
...env
}
const ast = source(file)
const text = ast.statements
.filter((n) => ts.isFunctionDeclaration(n) && names.includes(n.name!.text))
.map((n) => n.getText(ast))
.join('\n')
const js = ts.transpile(text, { target: ts.ScriptTarget.ES2022 })
return new Function(
...Object.keys(env),
`${js}; return { ${names.filter((n) => text.includes('function ' + n + '(')).join(',')} }`
)(...Object.values(env))
}
function expression(file: string, target: string, env: Record<string, any>) {
const ast = source(file)
let result = ''
function visit(node: ts.Node) {
if (ts.isBinaryExpression(node) && node.left.getText(ast) === target)
result = node.right.getText(ast)
if (ts.isVariableDeclaration(node) && node.name.getText(ast) === target && node.initializer)
result = node.initializer.getText(ast)
node.forEachChild(visit)
}
visit(ast)
assert.ok(result, `missing ${target}`)
const js = ts.transpile(`const result = ${result}`, { target: ts.ScriptTarget.ES2022 })
return new Function(...Object.keys(env), `${js}; return result`)(...Object.values(env))
}
function presignEnv(transfers: any[] = []) {
const writes: any[] = []
const warnings: string[] = []
const env: Record<string, any> = {
form: {
motherName: '测试',
sampleTypeIds: ['sample'],
transfers,
orderDate: '',
expectedDeliveryDate: '',
regionId: '',
hospitalId: '',
sampleCollectTime: '',
collectorUserId: '',
regionManagerUserId: '',
payMode: 'dj',
contractAmount: null,
orderStatus: 'active',
remark: ''
},
props: { orderId: 'order-1' },
saving: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v), success: () => {} },
fetchUpdatePresignOrder: async (_: string, body: any) =>
writes.push(JSON.parse(JSON.stringify(body))),
fetchUpdatePresignTransfer: async (_: string, body: any) =>
writes.push(JSON.parse(JSON.stringify(body))),
fetchCreatePresignTransfer: async (_: string, body: any) => {
writes.push(body)
return { id: 'new' }
},
fetchSubmitPresignTransferFinance: async () => {},
submitDfjTransfersOnSave: async () => [],
emit: () => {}
}
return { env, writes, warnings }
}
test('预签约清空可选字段后,更新请求仍携带空值', async () => {
const { env, writes } = presignEnv()
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
for (const key of [
'expectedDeliveryDate',
'regionId',
'hospitalId',
'sampleCollectTime',
'collectorUserId',
'regionManagerUserId',
'remark'
])
assert.equal(writes[0][key], '', key)
assert.equal(writes[0].contractAmount, null)
})
test('预签约保存转账清空凭证/日期/备注后请求携带空值', async () => {
const { env, writes } = presignEnv([
{ id: 't', payStatus: 'dfj', attachments: [], transferTime: '', remark: '' }
])
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes[1].transferAttachments, '')
assert.equal(writes[1].transferTime, '')
assert.equal(writes[1].remark, '')
})
test('预签约提交财务前同步清空凭证', async () => {
const { env, writes } = presignEnv()
await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
id: 't',
payStatus: 'dfj',
attachments: []
})
assert.equal(writes[0].transferAttachments, '')
})
test('预签约凭证上传时禁止保存订单', async () => {
const { env, writes, warnings } = presignEnv([
{ payStatus: 'dfj', attachments: [{ _uploading: true }] }
])
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
test('预签约凭证上传时禁止提交财务', async () => {
const { env, writes, warnings } = presignEnv()
await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
id: 't',
payStatus: 'dfj',
attachments: [{ _uploading: true }]
})
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
for (const attachments of [[], [{ fileUrl: 'new.pdf' }], undefined]) {
test(`协议整体保存使用当前附件数组(${JSON.stringify(attachments)})`, () => {
const payments = expression(protocol, 'payload.samplePayments', {
formData: {
samplePayments: [
{ schedules: [{ transfers: [{ transferAttachments: 'old.pdf', attachments }] }] }
]
},
toIntOrUndefined: () => undefined,
toFloatOrUndefined: () => undefined,
parseInstallmentRatios: () => []
})
assert.equal(
payments[0].schedules[0].transfers[0].transferAttachments,
attachments ? attachments.map((f) => f.fileUrl).join(',') : 'old.pdf'
)
})
}
test('样本缴费转账行保存删除全部凭证', async () => {
let written: any
const tr = {
id: 't',
transferType: 'bank',
transferAmount: 1,
attachments: [],
transferAttachments: 'old.pdf'
}
const env = {
props: { protocolId: 'p', modelValue: [{ schedules: [{ id: 's', transfers: [tr] }] }] },
savingKey: { value: '' },
isTransferLocked: () => false,
emitChange: () => {},
ElMessage: { warning: () => {}, success: () => {} },
fetchUpdatePaymentTransfer: async (_: string, body: any) => {
written = JSON.parse(JSON.stringify(body))
return { transferAttachments: '', attachments: [] }
}
}
await functions(
panel,
[
'saveTransferRow',
'getTransfer',
'validateTransferRow',
'transferRowKey',
'attachmentsToUrls',
'ensureTransferAttachments',
'applySavedTransfer'
],
env
).saveTransferRow(0, 0, 0)
assert.equal(written.transferAttachments, '')
assert.equal(tr.transferAttachments, '')
})
for (const file of [presign, panel]) {
for (const fail of [false, true]) {
test(`${file} 上传${fail ? '失败' : '成功'}时删除前一个附件不应错位`, async () => {
let resolve!: (value: any) => void
let reject!: (error: Error) => void
const pending = new Promise((yes, no) => {
resolve = yes
reject = no
})
const tr = { attachments: [{ id: 'old', fileUrl: 'old.pdf' }] }
const env = {
form: { transfers: [tr] },
props: { modelValue: [{ schedules: [{ transfers: [tr] }] }] },
emitChange: () => {},
fetchUploadAttachment: () => pending,
ElMessage: { warning: () => {}, error: () => {} }
}
const fns = functions(
file,
['handleTransferAttachmentChange', 'getTransfer', 'ensureTransferAttachments'],
env
)
const upload = { raw: new File(['x'], 'new.pdf') }
const saving =
file === presign
? fns.handleTransferAttachmentChange(0, upload)
: fns.handleTransferAttachmentChange(0, 0, 0, upload)
tr.attachments.splice(0, 1)
if (fail) reject(new Error('upload failed'))
else resolve({ id: 'new', fileUrl: 'new.pdf' })
await saving
assert.equal(tr.attachments.length, fail ? 0 : 1)
if (!fail) assert.equal(tr.attachments[0].fileUrl, 'new.pdf')
})
}
}
for (const handler of ['uploadProtocolAttachmentFile', 'handleDetailAttachmentChange']) {
for (const fail of [false, true]) {
test(`协议 ${handler} 上传${fail ? '失败' : '成功'}不受前项删除影响`, async () => {
let resolve!: (value: any) => void
let reject!: (error: Error) => void
const pending = new Promise((yes, no) => {
resolve = yes
reject = no
})
const list = [{ id: 'old', fileUrl: 'old.pdf' }]
const env = {
formData: { paymentDetails: [{ attachments: list }] },
fetchUploadAttachment: () => pending,
nextTick: (cb: () => void) => cb(),
ElMessage: { error: () => {} }
}
const fns = functions(protocol, [handler], env)
const upload = { raw: new File(['x'], 'new.pdf') }
const saving =
handler === 'uploadProtocolAttachmentFile'
? fns[handler](upload, list, 'protocol', {})
: fns[handler](0, upload)
list.splice(0, 1)
if (fail) reject(new Error('upload failed'))
else resolve({ id: 'new', fileUrl: 'new.pdf' })
await saving
assert.equal(list.length, fail ? 0 : 1)
if (!fail) assert.equal(list[0].fileUrl, 'new.pdf')
})
}
}
for (const field of [
'attachments',
'consentAttachments',
'healthSurveyAttachments',
'paymentDetails',
'samplePayments'
]) {
test(`协议 ${field} 上传中禁止整体保存`, async () => {
let validated = false
const warnings: string[] = []
const file = { _uploading: true }
const formData: any = {
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
paymentDetails: [],
samplePayments: []
}
formData[field] =
field === 'paymentDetails'
? [{ attachments: [file] }]
: field === 'samplePayments'
? [{ schedules: [{ transfers: [{ attachments: [file] }] }] }]
: [file]
const env = {
formData,
formRef: {
value: {
validate: async () => {
validated = true
throw new Error('validation')
}
}
},
submitLoading: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v) }
}
await functions(protocol, ['handleSubmit'], env).handleSubmit()
assert.equal(validated, false)
assert.equal(warnings.length, 1)
})
}
test('正式协议清空主单附件、客户和采集字段后提交空值', () => {
const formData = new Proxy(
{
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
samples: [],
protocolAttachments: 'old.pdf',
consentFormUrls: 'old-consent.pdf',
healthSurveyFormUrls: 'old-health.pdf'
},
{ get: (o, key) => (key in o ? o[key as keyof typeof o] : '') }
)
const payload = JSON.parse(
JSON.stringify(
expression(protocol, 'payload', {
formData,
protocolAttachmentUrls: '',
consentAttachmentUrls: '',
healthSurveyAttachmentUrls: ''
})
)
)
for (const key of ['protocolAttachments', 'consentFormUrls', 'healthSurveyFormUrls', 'remark'])
assert.equal(payload[key], '', key)
for (const key of ['motherPhone', 'motherEmail', 'fatherPhone', 'newbornIdCard'])
assert.equal(payload.partyCustomer[key], '', key)
for (const key of ['hospitalId', 'medicalStaff', 'sampleCollectTime'])
assert.equal(payload.collectionInfo[key], '', key)
})
test('协议详情加载失败后禁止保存未加载的表单', async () => {
let validated = false
const warnings: string[] = []
const env = {
isEdit: { value: true },
editDetailLoaded: { value: false },
formData: {
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
paymentDetails: [],
samplePayments: []
},
formRef: {
value: {
validate: async () => {
validated = true
throw new Error('validation')
}
}
},
submitLoading: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v) }
}
await functions(protocol, ['handleSubmit'], env).handleSubmit()
assert.equal(validated, false)
assert.equal(warnings.length, 1)
})
test('协议详情读取失败保持未就绪并提示重试', async () => {
const messages: string[] = []
const ready = { value: true }
const env = {
props: { listRow: { id: 'p' }, editId: 'p' },
isEdit: { value: false },
dialogTitle: { value: '' },
editingId: { value: '' },
editDetailLoaded: ready,
formData: {},
resetForm: () => {},
dialogFormLoading: { value: false },
loadEthnicOptions: async () => {},
fetchCollectionProtocolDetail: async () => {
throw new Error('network')
},
ElMessage: { error: (v: string) => messages.push(v) },
emit: () => {}
}
await functions(protocol, ['handleOpenForEdit'], env).handleOpenForEdit()
assert.equal(ready.value, false)
assert.equal(messages.length, 1)
})
test('编辑后新增协议不沿用上一条协议ID', () => {
const editingId = { value: 'previous-protocol' }
const env = {
editingId,
isEdit: { value: true },
dialogTitle: { value: '' },
resetForm: () => {},
formData: {},
DEFAULT_MOTHER_ETHNIC: '',
loadEthnicOptions: () => {},
userStore: { getUserInfo: {} }
}
functions(protocol, ['handleOpenForAdd'], env).handleOpenForAdd()
assert.equal(editingId.value, null)
})
function presignOpenWatcher(env: Record<string, any>) {
const ast = source(presign)
const statement = ast.statements.find(
(n) =>
ts.isExpressionStatement(n) &&
ts.isCallExpression(n.expression) &&
n.expression.expression.getText(ast) === 'watch'
)!
let callback: (open: boolean) => Promise<void>
new Function(
...Object.keys(env),
'watch',
ts.transpile(statement.getText(ast), { target: ts.ScriptTarget.ES2022 })
)(...Object.values(env), (_: unknown, cb: typeof callback) => {
callback = cb
})
return callback!
}
test('预签约从字典加载开始即处于加载中,全部初始化后才就绪', async () => {
let resolve!: () => void
const pending = new Promise<void>((yes) => {
resolve = yes
})
const loading = { value: false }
const formReady = { value: true }
const events: string[] = []
const open = presignOpenWatcher({
props: { orderId: 'p' },
loading,
formReady,
resetForm: () => {},
loadLookups: () => pending,
loadDetail: async () => {
events.push('detail')
},
ElMessage: { error: () => {} },
emit: () => {}
})
const initialization = open(true)
const during = { loading: loading.value, ready: formReady.value }
resolve()
await initialization
assert.deepEqual(during, { loading: true, ready: false })
assert.deepEqual(events, ['detail'])
assert.equal(loading.value, false)
assert.equal(formReady.value, true)
})
for (const stage of ['lookups', 'detail']) {
test(`预签约${stage}加载失败保持未就绪并提示重试`, async () => {
const loading = { value: false }
const formReady = { value: true }
const errors: string[] = []
const open = presignOpenWatcher({
props: { orderId: 'p' },
loading,
formReady,
resetForm: () => {},
loadLookups: async () => {
if (stage === 'lookups') throw new Error('network')
},
loadDetail: async () => {
if (stage === 'detail') throw new Error('network')
},
ElMessage: { error: (v: string) => errors.push(v) },
emit: () => {}
})
await open(true).catch(() => {})
assert.equal(formReady.value, false)
assert.equal(loading.value, false)
assert.equal(errors.length, 1)
})
}
for (const state of [
{ loading: true, ready: false },
{ loading: false, ready: false }
]) {
test(`预签约未初始化成功不得保存:${JSON.stringify(state)}`, async () => {
const { env, writes, warnings } = presignEnv()
env.loading = { value: state.loading }
env.formReady = { value: state.ready }
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
}