bx-child-identity.test.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFileSync } from 'node:fs'
  4. import ts from 'typescript'
  5. function fn(path: string, name: string, context: Record<string, any> = {}) {
  6. const source = readFileSync(new URL(path, import.meta.url), 'utf8')
  7. const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1] || source
  8. const ast = ts.createSourceFile('page.ts', script, ts.ScriptTarget.Latest, true)
  9. const node = ast.statements.find((s) => ts.isFunctionDeclaration(s) && s.name?.text === name)
  10. assert.ok(node, name)
  11. const js = ts.transpileModule(node.getText(ast), {
  12. compilerOptions: { target: ts.ScriptTarget.ES2022 }
  13. }).outputText
  14. return new Function(...Object.keys(context), `${js}; return ${name}`)(...Object.values(context))
  15. }
  16. const detail = {
  17. id: 'detail-a',
  18. expenseItemId: 'expense',
  19. amount: 10,
  20. remark: 'Reason',
  21. occurDate: '2026-01-01'
  22. }
  23. const webPage = '../src/views/bx/reimburse/apply/index.vue'
  24. const nativePage = '../../miniprogram/pages/bx/reimburse/apply.vue'
  25. for (const [editing, occurDate] of [[true, detail.occurDate], [false, detail.occurDate], [true, ''], [false, '']] as const) {
  26. test(`Web/H5 expense payload ${editing ? 'preserves edit ID' : 'clears source ID on create'} with ${occurDate || 'cleared date'}`, () => {
  27. const build = fn(webPage, 'buildAddPayload', {
  28. addForm: { details: [{ ...detail, occurDate }], remark: '', reimburseDate: '2026-01-01' },
  29. selectedInvoicePicks: { value: [] },
  30. editingId: { value: editing ? 'apply' : null },
  31. ElMessage: { warning() {} }
  32. })
  33. const row = build().details[0]
  34. assert.equal(row.id, editing ? 'detail-a' : undefined)
  35. assert.equal(row.remark, 'Reason')
  36. assert.equal(row.occurDate, occurDate || null)
  37. })
  38. test(`Native expense payload ${editing ? 'preserves edit ID' : 'clears source ID on create'} with ${occurDate || 'cleared date'}`, async () => {
  39. let payload: any
  40. const save = async (...args: any[]) => {
  41. payload = args.at(-1)
  42. return {
  43. data: {
  44. id: 'apply',
  45. details: [
  46. {
  47. id: 'server-detail',
  48. expenseId: 'expense',
  49. amount: 10,
  50. remark: 'Reason',
  51. occurDate: '2026-01-01'
  52. }
  53. ]
  54. }
  55. }
  56. }
  57. const context = {
  58. validateReimburseApplyForm: () => [{ ...detail, occurDate }],
  59. uploading: { value: false },
  60. submitting: { value: false },
  61. editId: { value: editing ? 'apply' : '' },
  62. savedId: { value: '' },
  63. buildInvoicePayload: () => ({}),
  64. form: { details: [{ ...detail }], title: '', remark: '', applicantId: '', reimburseDate: '' },
  65. reimburseApi: { update: save, create: save },
  66. bindNewAttachments: async () => {},
  67. uni: { showModal() {}, showToast() {} }
  68. }
  69. await fn(nativePage, 'onSubmit', context)()
  70. assert.ok(payload)
  71. assert.equal(payload.details[0].id, editing ? 'detail-a' : undefined)
  72. assert.equal(payload.details[0].occurDate, occurDate || null)
  73. assert.equal(
  74. context.form.details[0].id,
  75. 'server-detail',
  76. 'continued editing uses returned child ID'
  77. )
  78. })
  79. }
  80. for (const page of ['reimburse', 'advance']) {
  81. test(`${page} workflow payload retains node ID and action`, () => {
  82. const build = fn(`../src/views/bx/workflow/${page}/index.vue`, 'buildNodePayload')
  83. const [row] = build([{ id: 'node-a', nodeName: 'Review', actionType: 'disburse' }])
  84. assert.equal(row.id, 'node-a')
  85. assert.equal(row.actionType, 'disburse')
  86. })
  87. }
  88. test('Native workflow payload retains node ID and action', () => {
  89. const build = fn('../../miniprogram/components/bx-ui/bx-wf-config-page.vue', 'buildNodePayload')
  90. const [row] = build([{ id: 'node-a', nodeName: 'Review', actionType: 'disburse' }])
  91. assert.equal(row.id, 'node-a')
  92. assert.equal(row.actionType, 'disburse')
  93. })
  94. test('Web/H5 save response refreshes IDs before submit retry', async () => {
  95. const addForm: any = { details: [{ ...detail }] }
  96. const editingId = { value: null }
  97. const save = fn(webPage, 'saveAddForm', {
  98. addForm,
  99. editingId,
  100. pickBxData: (res: any) => res.data,
  101. reimburseApi: {
  102. create: async () => ({
  103. data: {
  104. id: 'apply',
  105. details: [
  106. {
  107. id: 'server-detail',
  108. expenseId: 'expense',
  109. amount: 10,
  110. remark: 'Reason',
  111. occurDate: '2026-01-01'
  112. }
  113. ]
  114. }
  115. })
  116. }
  117. })
  118. assert.equal(await save({}), 'apply')
  119. assert.equal(editingId.value, 'apply')
  120. assert.equal(addForm.details[0].id, 'server-detail')
  121. })
  122. test('Native edit loading retains detail identity', async () => {
  123. const form: any = {}
  124. const load = fn(nativePage, 'loadEditData', {
  125. form,
  126. reimburseApi: {
  127. getById: async () => ({
  128. data: { details: [{ id: 'detail-a', expenseId: 'expense', amount: 10 }] }
  129. })
  130. },
  131. userStore: { userInfo: { id: 'user' } },
  132. todayYmd: () => '2026-01-01',
  133. refreshWallet: async () => {},
  134. selectedInvoicePicks: { value: [] },
  135. kickAttachPreviewLoads() {},
  136. attachmentApi: { listByBiz: async () => ({ data: [] }) },
  137. files: { value: [] },
  138. uni: {
  139. showToast() {
  140. assert.fail('edit loading failed')
  141. }
  142. }
  143. })
  144. await load('apply')
  145. assert.equal(form.details[0].id, 'detail-a')
  146. })
  147. test('Web/H5 edit loading retains detail identity', async () => {
  148. const addForm: any = {}
  149. const load = fn(webPage, 'openEditDialog', {
  150. addForm,
  151. reimburseApi: {
  152. getById: async () => ({
  153. data: { details: [{ id: 'detail-a', expenseId: 'expense', amount: 10 }] }
  154. })
  155. },
  156. selectedInvoicePicks: { value: [] },
  157. applyAttachments: { value: [] },
  158. currentUserId: () => 'user',
  159. todayYmd: () => '2026-01-01',
  160. titleAutoLinked: { value: false },
  161. isAutoReimburseTitle: () => false,
  162. editingId: { value: null },
  163. dialogs: {},
  164. loadApplicantOptions: async () => {},
  165. refreshWalletBalance: async () => {},
  166. loadSelectedPicksFromInvoices: async () => {},
  167. loadBxAttachments: async () => [],
  168. ElMessage: {
  169. error() {
  170. assert.fail('edit loading failed')
  171. }
  172. }
  173. })
  174. await load({ id: 'apply' })
  175. assert.equal(addForm.details[0].id, 'detail-a')
  176. })
  177. test('Recharge workflow payload retains node ID for rename', () => {
  178. const build = fn('../src/views/pl/vehicle-recharge-workflow/index.vue', 'buildNodePayload')
  179. const [row] = build([{ id: 'node-a', nodeName: 'Renamed', actionType: 'disburse' }])
  180. assert.equal(row.id, 'node-a')
  181. assert.equal(row.actionType, 'disburse')
  182. })