mobile-pl-plan-arrange.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 } from 'vue'
  7. function evaluate(file: string, names: string[], b: Record<string, any>) {
  8. const ast = ts.createSourceFile(
  9. 'x.ts',
  10. parse(readFileSync(new URL(file, import.meta.url), 'utf8')).descriptor.scriptSetup!.content,
  11. ts.ScriptTarget.Latest,
  12. true
  13. )
  14. const text = names
  15. .map((name) => {
  16. const n = ast.statements.find((n) =>
  17. ts.isFunctionDeclaration(n)
  18. ? n.name?.text === name
  19. : ts.isVariableStatement(n) &&
  20. n.declarationList.declarations.some((d) => d.name.getText(ast) === name)
  21. )
  22. assert.ok(n, name)
  23. return n.getText(ast)
  24. })
  25. .join('\n')
  26. return new Function(
  27. ...Object.keys(b),
  28. ts.transpile(text, { target: ts.ScriptTarget.ESNext }) + ';return {' + names.join(',') + '}'
  29. )(...Object.values(b))
  30. }
  31. const file = '../src/components/business/pl-plan-form-dialog/index.vue'
  32. function harness(overrides: any = {}) {
  33. const writes: string[] = [],
  34. events: any[] = []
  35. const yes = () => true,
  36. noop = () => {},
  37. b: any = {
  38. ref,
  39. isPageVariant: ref(true),
  40. pageReady: ref(true),
  41. mobileMetadata: { ready: yes },
  42. mirrorPlanProductsFromLoad: noop,
  43. buildMobileProductLinks: noop,
  44. loadForm: ref({ products: [] }),
  45. deliveryForms: ref([]),
  46. formData: { products: [], vehicleId: 'v1' },
  47. pageLoadGeneration: 1,
  48. isPageSubmitCurrent: yes,
  49. isPageLoadCurrent: yes,
  50. submitLoading: ref(false),
  51. mobileSavedPlanId: ref(''),
  52. mobileCreateUncertain: ref(false),
  53. props: {},
  54. isEdit: ref(false),
  55. basicFormRef: ref({ validate: async () => true }),
  56. validateBusinessRules: yes,
  57. prefetchStopContractProducts: async () => {},
  58. validateSingleContractPerStop: yes,
  59. validateContractPackageSpecs: yes,
  60. validateStopContractsRequired: yes,
  61. getArrangePrerequisiteGaps: () => [],
  62. validateStopProductsForArrange: yes,
  63. buildPlanCommonPayload: () => ({}),
  64. planApi: {
  65. createFull: async () => {
  66. writes.push('create')
  67. return { id: 'p1' }
  68. },
  69. update: async () => {
  70. writes.push('update')
  71. }
  72. },
  73. pickBxData: (x: any) => x,
  74. showPlanSaveWarnings: noop,
  75. ElMessage: { warning: noop, error: noop, success: noop },
  76. innerVisible: ref(true),
  77. emit: (...a: any[]) => events.push(a),
  78. performArrangeAfterSave: async () => {
  79. writes.push('arrange')
  80. throw Error('get failed')
  81. },
  82. ...overrides
  83. }
  84. return { ...evaluate(file, ['handleSubmit'], b), b, writes, events }
  85. }
  86. test('create success then GET failure retains identity and emits saved result', async () => {
  87. const h = harness()
  88. await h.handleSubmit('saveAndArrange')
  89. assert.equal(h.b.mobileSavedPlanId.value, 'p1')
  90. assert.deepEqual(h.events.at(-1), ['success', { planId: 'p1', arranged: false }])
  91. })
  92. test('double click creates once', async () => {
  93. const h = harness()
  94. await Promise.all([h.handleSubmit('saveAndArrange'), h.handleSubmit('saveAndArrange')])
  95. assert.equal(h.writes.filter((x) => x === 'create').length, 1)
  96. })
  97. for (const reason of ['certificate', 'overflow', 'cancel', 'arrange500', 'get'])
  98. test('actual arrange handler preserves saved ID after ' + reason, async () => {
  99. const h = harness()
  100. Object.assign(h.b, {
  101. loadedPlanSnapshot: ref(null),
  102. selectedDriverId: ref(''),
  103. selectedEscortId: ref(''),
  104. freightCalc: {},
  105. getUnitLabel: (x: any) => x,
  106. enrichPlanProductsHazardFromCrm: async (x: any) => x,
  107. buildMissingHazardCertReadyMessage: () => (reason === 'certificate' ? '缺少证件' : ''),
  108. createArrangePresenter: () => async () => {},
  109. ElMessageBox: { alert: async () => {} },
  110. planHasAnyDibangSelected: () => false,
  111. loadArrangeHistoricalFlags: async () => ({}),
  112. confirmBeforePlanArrange: async () => {
  113. if (reason === 'cancel') throw 'cancel'
  114. },
  115. buildArrangePayloadFromPlanForm: () => ({}),
  116. submitPlanArrange: async () => {
  117. throw {
  118. response: {
  119. data: {
  120. detail: { code: reason === 'overflow' ? 'CONTRACT_QTY_OVERFLOW' : 'SERVER_ERROR' }
  121. }
  122. }
  123. }
  124. }
  125. })
  126. h.b.planApi.get = async () => {
  127. if (reason === 'get') throw Error('GET failed')
  128. return { id: 'p1', products: [], transportStatus: 'arranged' }
  129. }
  130. h.b.performArrangeAfterSave = evaluate(
  131. file,
  132. ['performArrangeAfterSave'],
  133. h.b
  134. ).performArrangeAfterSave
  135. const handler = evaluate(file, ['handleSubmit'], h.b).handleSubmit
  136. await handler('saveAndArrange')
  137. assert.equal(h.b.mobileSavedPlanId.value, 'p1')
  138. await handler('saveAndArrange')
  139. assert.equal(h.writes.filter((x) => x === 'create').length, 1)
  140. assert.deepEqual(h.events.at(-1), ['success', { planId: 'p1', arranged: false }])
  141. })
  142. test('uncertain create locks retry', async () => {
  143. let count = 0
  144. const h = harness({
  145. planApi: {
  146. createFull: async () => {
  147. count++
  148. throw Error('network')
  149. }
  150. }
  151. })
  152. await h.handleSubmit('saveAndArrange')
  153. await h.handleSubmit('saveAndArrange')
  154. assert.equal(count, 1)
  155. assert.equal(h.b.mobileCreateUncertain.value, true)
  156. })
  157. test('adjustment only updates', async () => {
  158. const h = harness({ props: { planId: 'existing' }, isEdit: ref(true) })
  159. await h.handleSubmit('rearrange')
  160. assert.deepEqual(h.writes, ['update', 'arrange'])
  161. })
  162. test('quick create selects returned vehicle without resetting stop choices or unsaved fields', async () => {
  163. const formData = { vehicleId: 'old', remark: 'unsaved' },
  164. calls: any[] = []
  165. const b = {
  166. isPageVariant: ref(true),
  167. formData,
  168. pageLoadGeneration: 1,
  169. isPageSubmitCurrent: () => true,
  170. ensureSelectedVehicleOption: async () => {},
  171. handleVehicleChange: async (...args: any[]) => calls.push(args),
  172. handleVehicleLibrarySelect: async () => {},
  173. ElMessage: { warning: () => {} }
  174. }
  175. const done = evaluate(file, ['handlePasteRecognizeDone'], b).handlePasteRecognizeDone
  176. await done({ vehicleId: 'new' })
  177. assert.equal(formData.vehicleId, 'new')
  178. assert.equal(formData.remark, 'unsaved')
  179. assert.equal(calls[0][1].syncStopDibang, false)
  180. })
  181. test('explicit validation rejection unlocks correction retry', async () => {
  182. let count = 0
  183. const h = harness({ planApi: { createFull: async () => { count++; throw { response: { status: 422 } } } } })
  184. await h.handleSubmit('saveAndArrange').catch(() => {})
  185. assert.equal(h.b.mobileCreateUncertain.value, false)
  186. await h.handleSubmit('saveAndArrange').catch(() => {})
  187. assert.equal(count, 2)
  188. })
  189. test('new plan session clears saved identity and uncertain result', async () => {
  190. const b = { isPageVariant: ref(true), mobileSavedPlanId: ref('old'), mobileCreateUncertain: ref(true), pageLoadGeneration: 1, pageReady: ref(true), arrangeSheetsRef: ref(null),
  191. props: { visible: false }, handleOpened: async () => {} }
  192. const run = evaluate(file, ['handleMobilePlanSessionChange'], b).handleMobilePlanSessionChange
  193. await run()
  194. assert.equal(b.mobileSavedPlanId.value, '')
  195. assert.equal(b.mobileCreateUncertain.value, false)
  196. assert.equal(b.pageReady.value, false)
  197. })
  198. function arrangeFunctions(bindings: Record<string, any>) {
  199. const source = readFileSync(new URL('../src/composables/usePlPlanArrangeSubmit.ts', import.meta.url), 'utf8')
  200. const ast = ts.createSourceFile('arrange.ts', source, ts.ScriptTarget.Latest, true)
  201. const text = ast.statements.filter(n => ts.isFunctionDeclaration(n) && ['confirmBeforePlanArrange', 'submitPlanArrange'].includes(n.name!.text)).map(n => n.getText(ast).replace(/^export /, '')).join('\n')
  202. return new Function(...Object.keys(bindings), ts.transpile(text, { target: ts.ScriptTarget.ESNext }) + ';return {confirmBeforePlanArrange,submitPlanArrange}')(...Object.values(bindings))
  203. }
  204. test('arrange presenter replaces HTML dialogs while PC keeps original calls and payload', async () => {
  205. const calls:any[] = [], payload = { vehicleId:'v', driverId:undefined, escortId:undefined }
  206. const b = { planRequiresSiteLink:()=>false, ElMessageBox:{confirm:async(...args:any[])=>calls.push(['pc',...args])}, transportApi:{arrange:async(id:string,body:any)=>{calls.push(['api',id,body]);return {contractQtyReminder:{items:[{remainingQty:2}]}}}}, pickBxData:(x:any)=>x, showContractQtyReminderIfAny:async()=>calls.push(['pc-reminder']), showContractQtyViolationIfMatch:async()=>calls.push(['pc-violation']), extractContractQtyViolationDetail:(e:any)=>e.response?.data?.detail?.code==='CONTRACT_QTY_OVERFLOW'?e.response.data.detail:null }
  207. const f=arrangeFunctions(b), presenter=async(request:any)=>{calls.push(['mobile',request])}
  208. await f.confirmBeforePlanArrange({isRearrange:true,plan:{},presenter})
  209. assert.equal(calls[0][0],'mobile');assert.equal(calls[0][1].confirmText,'继续调整')
  210. await f.submitPlanArrange('p',payload,presenter)
  211. assert.equal(calls[1][2],payload);assert.equal(calls[2][1].items[0].remainingQty,2)
  212. await f.confirmBeforePlanArrange({isRearrange:true,plan:{}})
  213. assert.deepEqual(calls[3],['pc','司机链接将更新,旧链接将失效。是否继续?','调整运输确认',{type:'warning',confirmButtonText:'继续调整',cancelButtonText:'取消'}])
  214. const failure={response:{data:{detail:{code:'CONTRACT_QTY_OVERFLOW',message:'超量',items:[{remainingQty:-1}]}}}}
  215. b.transportApi.arrange=async()=>{throw failure}
  216. await assert.rejects(f.submitPlanArrange('p',payload,presenter),e=>e===failure)
  217. assert.equal(calls.at(-1)[1].items[0].remainingQty,-1)
  218. })
  219. test('generation-bound presenter ignores old arrange response and session change settles old sheet',async()=>{
  220. let generation=1;const presented:any[]=[],closed:any[]=[]
  221. const b={isPageSubmitCurrent:(value:number)=>value===generation,arrangeSheetsRef:ref({present:async(value:any)=>presented.push(value),close:()=>closed.push(true)}),isPageVariant:ref(true),mobileSavedPlanId:ref('old'),mobileCreateUncertain:ref(true),pageLoadGeneration:1,pageReady:ref(true),props:{visible:false},handleOpened:async()=>{}}
  222. const f=evaluate(file,['createArrangePresenter','handleMobilePlanSessionChange'],b)
  223. const old=f.createArrangePresenter(1);await f.handleMobilePlanSessionChange();generation=2
  224. await old({title:'old response'});assert.equal(presented.length,0);assert.equal(closed.length,1)
  225. await f.createArrangePresenter(2)({title:'new response'});assert.equal(presented[0].title,'new response')
  226. })