488 lines
16 KiB
TypeScript
488 lines
16 KiB
TypeScript
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)
|
||
})
|
||
}
|