test-cell-storage-form-regressions.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import { test } from 'node:test'
  4. import ts from 'typescript'
  5. const base = new URL('../src/views/cell-storage/', import.meta.url)
  6. const presign = 'presign-order/PresignOrderFormDialog.vue'
  7. const panel = 'protocol/SamplePaymentPanel.vue'
  8. const protocol = 'protocol/ProtocolFormDialog.vue'
  9. function source(file: string) {
  10. const text = readFileSync(new URL(file, base), 'utf8').match(
  11. /<script setup lang="ts">([\s\S]*?)<\/script>/
  12. )![1]
  13. return ts.createSourceFile(file + '.ts', text, ts.ScriptTarget.Latest, true)
  14. }
  15. function functions(file: string, names: string[], env: Record<string, any>) {
  16. env = {
  17. isEdit: { value: false },
  18. editDetailLoaded: { value: true },
  19. loading: { value: false },
  20. formReady: { value: true },
  21. ...env
  22. }
  23. const ast = source(file)
  24. const text = ast.statements
  25. .filter((n) => ts.isFunctionDeclaration(n) && names.includes(n.name!.text))
  26. .map((n) => n.getText(ast))
  27. .join('\n')
  28. const js = ts.transpile(text, { target: ts.ScriptTarget.ES2022 })
  29. return new Function(
  30. ...Object.keys(env),
  31. `${js}; return { ${names.filter((n) => text.includes('function ' + n + '(')).join(',')} }`
  32. )(...Object.values(env))
  33. }
  34. function expression(file: string, target: string, env: Record<string, any>) {
  35. const ast = source(file)
  36. let result = ''
  37. function visit(node: ts.Node) {
  38. if (ts.isBinaryExpression(node) && node.left.getText(ast) === target)
  39. result = node.right.getText(ast)
  40. if (ts.isVariableDeclaration(node) && node.name.getText(ast) === target && node.initializer)
  41. result = node.initializer.getText(ast)
  42. node.forEachChild(visit)
  43. }
  44. visit(ast)
  45. assert.ok(result, `missing ${target}`)
  46. const js = ts.transpile(`const result = ${result}`, { target: ts.ScriptTarget.ES2022 })
  47. return new Function(...Object.keys(env), `${js}; return result`)(...Object.values(env))
  48. }
  49. function presignEnv(transfers: any[] = []) {
  50. const writes: any[] = []
  51. const warnings: string[] = []
  52. const env: Record<string, any> = {
  53. form: {
  54. motherName: '测试',
  55. sampleTypeIds: ['sample'],
  56. transfers,
  57. orderDate: '',
  58. expectedDeliveryDate: '',
  59. regionId: '',
  60. hospitalId: '',
  61. sampleCollectTime: '',
  62. collectorUserId: '',
  63. regionManagerUserId: '',
  64. payMode: 'dj',
  65. contractAmount: null,
  66. orderStatus: 'active',
  67. remark: ''
  68. },
  69. props: { orderId: 'order-1' },
  70. saving: { value: false },
  71. ElMessage: { warning: (v: string) => warnings.push(v), success: () => {} },
  72. fetchUpdatePresignOrder: async (_: string, body: any) =>
  73. writes.push(JSON.parse(JSON.stringify(body))),
  74. fetchUpdatePresignTransfer: async (_: string, body: any) =>
  75. writes.push(JSON.parse(JSON.stringify(body))),
  76. fetchCreatePresignTransfer: async (_: string, body: any) => {
  77. writes.push(body)
  78. return { id: 'new' }
  79. },
  80. fetchSubmitPresignTransferFinance: async () => {},
  81. submitDfjTransfersOnSave: async () => [],
  82. emit: () => {}
  83. }
  84. return { env, writes, warnings }
  85. }
  86. test('预签约清空可选字段后,更新请求仍携带空值', async () => {
  87. const { env, writes } = presignEnv()
  88. await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
  89. for (const key of [
  90. 'expectedDeliveryDate',
  91. 'regionId',
  92. 'hospitalId',
  93. 'sampleCollectTime',
  94. 'collectorUserId',
  95. 'regionManagerUserId',
  96. 'remark'
  97. ])
  98. assert.equal(writes[0][key], '', key)
  99. assert.equal(writes[0].contractAmount, null)
  100. })
  101. test('预签约保存转账清空凭证/日期/备注后请求携带空值', async () => {
  102. const { env, writes } = presignEnv([
  103. { id: 't', payStatus: 'dfj', attachments: [], transferTime: '', remark: '' }
  104. ])
  105. await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
  106. assert.equal(writes[1].transferAttachments, '')
  107. assert.equal(writes[1].transferTime, '')
  108. assert.equal(writes[1].remark, '')
  109. })
  110. test('预签约提交财务前同步清空凭证', async () => {
  111. const { env, writes } = presignEnv()
  112. await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
  113. id: 't',
  114. payStatus: 'dfj',
  115. attachments: []
  116. })
  117. assert.equal(writes[0].transferAttachments, '')
  118. })
  119. test('预签约凭证上传时禁止保存订单', async () => {
  120. const { env, writes, warnings } = presignEnv([
  121. { payStatus: 'dfj', attachments: [{ _uploading: true }] }
  122. ])
  123. await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
  124. assert.equal(writes.length, 0)
  125. assert.equal(warnings.length, 1)
  126. })
  127. test('预签约凭证上传时禁止提交财务', async () => {
  128. const { env, writes, warnings } = presignEnv()
  129. await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
  130. id: 't',
  131. payStatus: 'dfj',
  132. attachments: [{ _uploading: true }]
  133. })
  134. assert.equal(writes.length, 0)
  135. assert.equal(warnings.length, 1)
  136. })
  137. for (const attachments of [[], [{ fileUrl: 'new.pdf' }], undefined]) {
  138. test(`协议整体保存使用当前附件数组(${JSON.stringify(attachments)})`, () => {
  139. const payments = expression(protocol, 'payload.samplePayments', {
  140. formData: {
  141. samplePayments: [
  142. { schedules: [{ transfers: [{ transferAttachments: 'old.pdf', attachments }] }] }
  143. ]
  144. },
  145. toIntOrUndefined: () => undefined,
  146. toFloatOrUndefined: () => undefined,
  147. parseInstallmentRatios: () => []
  148. })
  149. assert.equal(
  150. payments[0].schedules[0].transfers[0].transferAttachments,
  151. attachments ? attachments.map((f) => f.fileUrl).join(',') : 'old.pdf'
  152. )
  153. })
  154. }
  155. test('样本缴费转账行保存删除全部凭证', async () => {
  156. let written: any
  157. const tr = {
  158. id: 't',
  159. transferType: 'bank',
  160. transferAmount: 1,
  161. attachments: [],
  162. transferAttachments: 'old.pdf'
  163. }
  164. const env = {
  165. props: { protocolId: 'p', modelValue: [{ schedules: [{ id: 's', transfers: [tr] }] }] },
  166. savingKey: { value: '' },
  167. isTransferLocked: () => false,
  168. emitChange: () => {},
  169. ElMessage: { warning: () => {}, success: () => {} },
  170. fetchUpdatePaymentTransfer: async (_: string, body: any) => {
  171. written = JSON.parse(JSON.stringify(body))
  172. return { transferAttachments: '', attachments: [] }
  173. }
  174. }
  175. await functions(
  176. panel,
  177. [
  178. 'saveTransferRow',
  179. 'getTransfer',
  180. 'validateTransferRow',
  181. 'transferRowKey',
  182. 'attachmentsToUrls',
  183. 'ensureTransferAttachments',
  184. 'applySavedTransfer'
  185. ],
  186. env
  187. ).saveTransferRow(0, 0, 0)
  188. assert.equal(written.transferAttachments, '')
  189. assert.equal(tr.transferAttachments, '')
  190. })
  191. for (const file of [presign, panel]) {
  192. for (const fail of [false, true]) {
  193. test(`${file} 上传${fail ? '失败' : '成功'}时删除前一个附件不应错位`, async () => {
  194. let resolve!: (value: any) => void
  195. let reject!: (error: Error) => void
  196. const pending = new Promise((yes, no) => {
  197. resolve = yes
  198. reject = no
  199. })
  200. const tr = { attachments: [{ id: 'old', fileUrl: 'old.pdf' }] }
  201. const env = {
  202. form: { transfers: [tr] },
  203. props: { modelValue: [{ schedules: [{ transfers: [tr] }] }] },
  204. emitChange: () => {},
  205. fetchUploadAttachment: () => pending,
  206. ElMessage: { warning: () => {}, error: () => {} }
  207. }
  208. const fns = functions(
  209. file,
  210. ['handleTransferAttachmentChange', 'getTransfer', 'ensureTransferAttachments'],
  211. env
  212. )
  213. const upload = { raw: new File(['x'], 'new.pdf') }
  214. const saving =
  215. file === presign
  216. ? fns.handleTransferAttachmentChange(0, upload)
  217. : fns.handleTransferAttachmentChange(0, 0, 0, upload)
  218. tr.attachments.splice(0, 1)
  219. if (fail) reject(new Error('upload failed'))
  220. else resolve({ id: 'new', fileUrl: 'new.pdf' })
  221. await saving
  222. assert.equal(tr.attachments.length, fail ? 0 : 1)
  223. if (!fail) assert.equal(tr.attachments[0].fileUrl, 'new.pdf')
  224. })
  225. }
  226. }
  227. for (const handler of ['uploadProtocolAttachmentFile', 'handleDetailAttachmentChange']) {
  228. for (const fail of [false, true]) {
  229. test(`协议 ${handler} 上传${fail ? '失败' : '成功'}不受前项删除影响`, async () => {
  230. let resolve!: (value: any) => void
  231. let reject!: (error: Error) => void
  232. const pending = new Promise((yes, no) => {
  233. resolve = yes
  234. reject = no
  235. })
  236. const list = [{ id: 'old', fileUrl: 'old.pdf' }]
  237. const env = {
  238. formData: { paymentDetails: [{ attachments: list }] },
  239. fetchUploadAttachment: () => pending,
  240. nextTick: (cb: () => void) => cb(),
  241. ElMessage: { error: () => {} }
  242. }
  243. const fns = functions(protocol, [handler], env)
  244. const upload = { raw: new File(['x'], 'new.pdf') }
  245. const saving =
  246. handler === 'uploadProtocolAttachmentFile'
  247. ? fns[handler](upload, list, 'protocol', {})
  248. : fns[handler](0, upload)
  249. list.splice(0, 1)
  250. if (fail) reject(new Error('upload failed'))
  251. else resolve({ id: 'new', fileUrl: 'new.pdf' })
  252. await saving
  253. assert.equal(list.length, fail ? 0 : 1)
  254. if (!fail) assert.equal(list[0].fileUrl, 'new.pdf')
  255. })
  256. }
  257. }
  258. for (const field of [
  259. 'attachments',
  260. 'consentAttachments',
  261. 'healthSurveyAttachments',
  262. 'paymentDetails',
  263. 'samplePayments'
  264. ]) {
  265. test(`协议 ${field} 上传中禁止整体保存`, async () => {
  266. let validated = false
  267. const warnings: string[] = []
  268. const file = { _uploading: true }
  269. const formData: any = {
  270. attachments: [],
  271. consentAttachments: [],
  272. healthSurveyAttachments: [],
  273. paymentDetails: [],
  274. samplePayments: []
  275. }
  276. formData[field] =
  277. field === 'paymentDetails'
  278. ? [{ attachments: [file] }]
  279. : field === 'samplePayments'
  280. ? [{ schedules: [{ transfers: [{ attachments: [file] }] }] }]
  281. : [file]
  282. const env = {
  283. formData,
  284. formRef: {
  285. value: {
  286. validate: async () => {
  287. validated = true
  288. throw new Error('validation')
  289. }
  290. }
  291. },
  292. submitLoading: { value: false },
  293. ElMessage: { warning: (v: string) => warnings.push(v) }
  294. }
  295. await functions(protocol, ['handleSubmit'], env).handleSubmit()
  296. assert.equal(validated, false)
  297. assert.equal(warnings.length, 1)
  298. })
  299. }
  300. test('正式协议清空主单附件、客户和采集字段后提交空值', () => {
  301. const formData = new Proxy(
  302. {
  303. attachments: [],
  304. consentAttachments: [],
  305. healthSurveyAttachments: [],
  306. samples: [],
  307. protocolAttachments: 'old.pdf',
  308. consentFormUrls: 'old-consent.pdf',
  309. healthSurveyFormUrls: 'old-health.pdf'
  310. },
  311. { get: (o, key) => (key in o ? o[key as keyof typeof o] : '') }
  312. )
  313. const payload = JSON.parse(
  314. JSON.stringify(
  315. expression(protocol, 'payload', {
  316. formData,
  317. protocolAttachmentUrls: '',
  318. consentAttachmentUrls: '',
  319. healthSurveyAttachmentUrls: ''
  320. })
  321. )
  322. )
  323. for (const key of ['protocolAttachments', 'consentFormUrls', 'healthSurveyFormUrls', 'remark'])
  324. assert.equal(payload[key], '', key)
  325. for (const key of ['motherPhone', 'motherEmail', 'fatherPhone', 'newbornIdCard'])
  326. assert.equal(payload.partyCustomer[key], '', key)
  327. for (const key of ['hospitalId', 'medicalStaff', 'sampleCollectTime'])
  328. assert.equal(payload.collectionInfo[key], '', key)
  329. })
  330. test('协议详情加载失败后禁止保存未加载的表单', async () => {
  331. let validated = false
  332. const warnings: string[] = []
  333. const env = {
  334. isEdit: { value: true },
  335. editDetailLoaded: { value: false },
  336. formData: {
  337. attachments: [],
  338. consentAttachments: [],
  339. healthSurveyAttachments: [],
  340. paymentDetails: [],
  341. samplePayments: []
  342. },
  343. formRef: {
  344. value: {
  345. validate: async () => {
  346. validated = true
  347. throw new Error('validation')
  348. }
  349. }
  350. },
  351. submitLoading: { value: false },
  352. ElMessage: { warning: (v: string) => warnings.push(v) }
  353. }
  354. await functions(protocol, ['handleSubmit'], env).handleSubmit()
  355. assert.equal(validated, false)
  356. assert.equal(warnings.length, 1)
  357. })
  358. test('协议详情读取失败保持未就绪并提示重试', async () => {
  359. const messages: string[] = []
  360. const ready = { value: true }
  361. const env = {
  362. props: { listRow: { id: 'p' }, editId: 'p' },
  363. isEdit: { value: false },
  364. dialogTitle: { value: '' },
  365. editingId: { value: '' },
  366. editDetailLoaded: ready,
  367. formData: {},
  368. resetForm: () => {},
  369. dialogFormLoading: { value: false },
  370. loadEthnicOptions: async () => {},
  371. fetchCollectionProtocolDetail: async () => {
  372. throw new Error('network')
  373. },
  374. ElMessage: { error: (v: string) => messages.push(v) },
  375. emit: () => {}
  376. }
  377. await functions(protocol, ['handleOpenForEdit'], env).handleOpenForEdit()
  378. assert.equal(ready.value, false)
  379. assert.equal(messages.length, 1)
  380. })
  381. test('编辑后新增协议不沿用上一条协议ID', () => {
  382. const editingId = { value: 'previous-protocol' }
  383. const env = {
  384. editingId,
  385. isEdit: { value: true },
  386. dialogTitle: { value: '' },
  387. resetForm: () => {},
  388. formData: {},
  389. DEFAULT_MOTHER_ETHNIC: '',
  390. loadEthnicOptions: () => {},
  391. userStore: { getUserInfo: {} }
  392. }
  393. functions(protocol, ['handleOpenForAdd'], env).handleOpenForAdd()
  394. assert.equal(editingId.value, null)
  395. })
  396. function presignOpenWatcher(env: Record<string, any>) {
  397. const ast = source(presign)
  398. const statement = ast.statements.find(
  399. (n) =>
  400. ts.isExpressionStatement(n) &&
  401. ts.isCallExpression(n.expression) &&
  402. n.expression.expression.getText(ast) === 'watch'
  403. )!
  404. let callback: (open: boolean) => Promise<void>
  405. new Function(
  406. ...Object.keys(env),
  407. 'watch',
  408. ts.transpile(statement.getText(ast), { target: ts.ScriptTarget.ES2022 })
  409. )(...Object.values(env), (_: unknown, cb: typeof callback) => {
  410. callback = cb
  411. })
  412. return callback!
  413. }
  414. test('预签约从字典加载开始即处于加载中,全部初始化后才就绪', async () => {
  415. let resolve!: () => void
  416. const pending = new Promise<void>((yes) => {
  417. resolve = yes
  418. })
  419. const loading = { value: false }
  420. const formReady = { value: true }
  421. const events: string[] = []
  422. const open = presignOpenWatcher({
  423. props: { orderId: 'p' },
  424. loading,
  425. formReady,
  426. resetForm: () => {},
  427. loadLookups: () => pending,
  428. loadDetail: async () => {
  429. events.push('detail')
  430. },
  431. ElMessage: { error: () => {} },
  432. emit: () => {}
  433. })
  434. const initialization = open(true)
  435. const during = { loading: loading.value, ready: formReady.value }
  436. resolve()
  437. await initialization
  438. assert.deepEqual(during, { loading: true, ready: false })
  439. assert.deepEqual(events, ['detail'])
  440. assert.equal(loading.value, false)
  441. assert.equal(formReady.value, true)
  442. })
  443. for (const stage of ['lookups', 'detail']) {
  444. test(`预签约${stage}加载失败保持未就绪并提示重试`, async () => {
  445. const loading = { value: false }
  446. const formReady = { value: true }
  447. const errors: string[] = []
  448. const open = presignOpenWatcher({
  449. props: { orderId: 'p' },
  450. loading,
  451. formReady,
  452. resetForm: () => {},
  453. loadLookups: async () => {
  454. if (stage === 'lookups') throw new Error('network')
  455. },
  456. loadDetail: async () => {
  457. if (stage === 'detail') throw new Error('network')
  458. },
  459. ElMessage: { error: (v: string) => errors.push(v) },
  460. emit: () => {}
  461. })
  462. await open(true).catch(() => {})
  463. assert.equal(formReady.value, false)
  464. assert.equal(loading.value, false)
  465. assert.equal(errors.length, 1)
  466. })
  467. }
  468. for (const state of [
  469. { loading: true, ready: false },
  470. { loading: false, ready: false }
  471. ]) {
  472. test(`预签约未初始化成功不得保存:${JSON.stringify(state)}`, async () => {
  473. const { env, writes, warnings } = presignEnv()
  474. env.loading = { value: state.loading }
  475. env.formReady = { value: state.ready }
  476. await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
  477. assert.equal(writes.length, 0)
  478. assert.equal(warnings.length, 1)
  479. })
  480. }