pl-plan-save-identity.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFileSync } from 'node:fs'
  4. import { parse } from '@vue/compiler-sfc'
  5. import ts from 'typescript'
  6. import { ref, nextTick } from 'vue'
  7. import * as stops from '../src/utils/pl-plan-stop-form'
  8. import * as persistence from '../src/views/m/pl/plan/mobilePlanPersistence'
  9. // Evaluate current working-tree handlers, with only external APIs/UI services stubbed.
  10. const source = parse(
  11. readFileSync(
  12. new URL('../src/components/business/pl-plan-form-dialog/index.vue', import.meta.url),
  13. 'utf8'
  14. )
  15. ).descriptor.scriptSetup!.content
  16. const tree = ts.createSourceFile('form.ts', source, ts.ScriptTarget.Latest, true)
  17. const names = [
  18. 'loadPlanForEdit',
  19. 'loadPlanForCopy',
  20. 'applyStopsFromPlan',
  21. 'enrichStopFormsDisplayFromPlanProducts',
  22. 'bootstrapStopsFromPlanProducts',
  23. 'mirrorPlanProductsFromLoad',
  24. 'mapPlanProductToFormRow',
  25. 'mapPlanProductToCopyFormRow',
  26. 'mapProductsToSubmitPayload',
  27. 'buildPlanCommonPayload',
  28. 'handleSubmit'
  29. ]
  30. const code = names
  31. .map((name) => {
  32. const node = tree.statements.find(
  33. (node) => ts.isFunctionDeclaration(node) && node.name?.text === name
  34. )
  35. assert.ok(node, name)
  36. return node.getText(tree)
  37. })
  38. .join('\n')
  39. const clone = (value: any) => JSON.parse(JSON.stringify(value))
  40. const line = {
  41. productId: 'sku',
  42. productNo: 'SKU',
  43. productName: '产品',
  44. unit: '吨',
  45. packageSpec: '桶',
  46. model: 'M',
  47. planQty: 5,
  48. quantity: 5
  49. }
  50. function harness(mobile: boolean) {
  51. const noop = () => {},
  52. yes = () => true,
  53. identity = async (rows: any[]) => rows
  54. let saved: any = {
  55. id: 'plan',
  56. mobileProductMetaVersion: 1,
  57. products: [{ ...line, id: 'pp' }],
  58. load: { id: 'load', products: [{ ...line, id: 'lp', planProductId: 'pp' }] },
  59. deliveries: [{ id: 'delivery', seq: 1, products: [{ ...line, id: 'dp', planProductId: 'pp' }] }]
  60. }
  61. const writes: any[] = [],
  62. events: any[] = []
  63. const b: any = {
  64. ...stops,
  65. ...persistence,
  66. nextTick,
  67. isPageVariant: ref(mobile),
  68. props: { planId: 'plan' },
  69. isEdit: ref(true),
  70. pageReady: ref(true),
  71. pageLoadGeneration: 1,
  72. assertPageLoadCurrent: noop,
  73. isPageLoadCurrent: yes,
  74. isPageSubmitCurrent: yes,
  75. suppressAddressMutex: ref(false),
  76. suppressStopProductSync: ref(false),
  77. stopFormsReady: ref(false),
  78. formData: { products: [] },
  79. loadForm: ref(stops.createEmptyLoadForm()),
  80. deliveryForms: ref([]),
  81. resetForm: noop,
  82. validateLoadedPagePlan: noop,
  83. formatDate: () => '',
  84. todayYmd: () => '2026-10-27',
  85. resolveCurrentUserId: () => 'u',
  86. resolveCurrentUserName: () => '',
  87. syncCommittedPartyIds: noop,
  88. loadedTransportStatus: ref(''),
  89. loadedFollowStatus: ref(''),
  90. loadedPlanSnapshot: ref(null),
  91. enrichPlanProductsHazardFromCrm: identity,
  92. enrichPlanProductLinesAlias: identity,
  93. freightCalc: { resetFreightCalc: noop },
  94. restoreFreightFromTransport: noop,
  95. selectedDriverId: ref(''),
  96. selectedEscortId: ref(''),
  97. vehicleBundle: ref(null),
  98. syncSelectedCustomerOptions: noop,
  99. reapplyStopDibangByVehiclePlate: noop,
  100. prepareLineAliasAndBrandForSubmit: noop,
  101. resolveLineProductName: (name: string) => name,
  102. buildTransportBlock: () => undefined,
  103. mobileMetadata: { ready: yes },
  104. mobileSavedPlanId: ref(''),
  105. mobileCreateUncertain: ref(false),
  106. submitLoading: ref(false),
  107. basicFormRef: ref({ validate: async () => true }),
  108. validateBusinessRules: yes,
  109. prefetchStopContractProducts: noop,
  110. validateSingleContractPerStop: yes,
  111. validateContractPackageSpecs: yes,
  112. validateStopContractsRequired: yes,
  113. pickBxData: (x: any) => x,
  114. showPlanSaveWarnings: noop,
  115. ElMessage: { warning: noop, error: noop, success: noop },
  116. innerVisible: ref(true),
  117. emit: (...args: any[]) => events.push(args),
  118. planApi: {
  119. get: async () => clone(saved),
  120. update: async (_id: string, payload: any) => {
  121. writes.push(clone(payload))
  122. // Server returns generated identities for genuinely new rows.
  123. saved = { ...clone(payload), id: 'plan', mobileProductMetaVersion: 1 }
  124. saved.products.forEach((row: any, i: number) => {
  125. row.id ||= `pp-new-${i}`
  126. })
  127. const resolve = (row: any) =>
  128. saved.products.find((pp: any) => pp.clientKey === row.planProductClientKey)?.id ||
  129. row.planProductId
  130. saved.load.id ||= 'load-new'
  131. saved.load.products.forEach((row: any, i: number) => {
  132. row.id ||= `lp-new-${i}`
  133. row.planProductId = resolve(row)
  134. })
  135. saved.deliveries.forEach((stop: any, i: number) => {
  136. stop.id ||= `delivery-new-${i}`
  137. stop.products.forEach((row: any, j: number) => {
  138. row.id ||= `dp-new-${i}-${j}`
  139. row.planProductId = resolve(row)
  140. })
  141. })
  142. return clone(saved)
  143. }
  144. }
  145. }
  146. const api = new Function(
  147. ...Object.keys(b),
  148. ts.transpile(code, { target: ts.ScriptTarget.ESNext }) + `;return {${names.join(',')}}`
  149. )(...Object.values(b))
  150. return { b, api, writes, events, saved: () => saved }
  151. }
  152. for (const mobile of [false, true]) {
  153. const platform = mobile ? 'H5' : 'Web'
  154. test(`${platform} actual edit/save/reopen/save keeps all existing and freshly assigned child IDs`, async () => {
  155. const h = harness(mobile)
  156. await h.api.loadPlanForEdit('plan')
  157. h.b.loadForm.value.products.push({
  158. ...line,
  159. packageSpec: '袋',
  160. id: undefined,
  161. planProductId: undefined
  162. })
  163. h.b.deliveryForms.value.push({
  164. ...stops.createEmptyDeliveryForm(2),
  165. products: [{ ...line, packageSpec: '袋' }]
  166. })
  167. await h.api.handleSubmit('save')
  168. assert.equal(h.b.innerVisible.value, false)
  169. assert.deepEqual(h.events.at(-1), ['success', { planId: 'plan', arranged: false }])
  170. const saved = clone(h.saved())
  171. await h.api.loadPlanForEdit('plan')
  172. await h.api.handleSubmit('save')
  173. const payload = h.writes[1]
  174. assert.deepEqual(
  175. payload.products.map((p: any) => p.id),
  176. saved.products.map((p: any) => p.id)
  177. )
  178. assert.equal(payload.load.id, saved.load.id)
  179. assert.deepEqual(
  180. payload.load.products.map((p: any) => p.id),
  181. saved.load.products.map((p: any) => p.id)
  182. )
  183. assert.deepEqual(
  184. payload.deliveries.map((d: any) => [d.id, ...d.products.map((p: any) => p.id)]),
  185. saved.deliveries.map((d: any) => [d.id, ...d.products.map((p: any) => p.id)])
  186. )
  187. assert.ok(payload.load.products.every((p: any) => p.planProductId))
  188. assert.ok(payload.deliveries.every((d: any) => d.products.every((p: any) => p.planProductId)))
  189. })
  190. test(`${platform} actual copy handler strips source parent/product IDs from submit payload`, async () => {
  191. const h = harness(mobile)
  192. await h.api.loadPlanForCopy('plan')
  193. const payload = h.api.buildPlanCommonPayload()
  194. assert.equal(payload.load.id, undefined)
  195. assert.ok(payload.products.every((p: any) => !p.id))
  196. assert.ok(payload.load.products.every((p: any) => !p.id && !p.planProductId))
  197. assert.ok(
  198. payload.deliveries.every(
  199. (d: any) => !d.id && d.products.every((p: any) => !p.id && !p.planProductId)
  200. )
  201. )
  202. })
  203. }