mobile-quote-form-detail-parity.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import test from 'node:test'
  4. import { parse } from '@vue/compiler-sfc'
  5. import ts from 'typescript'
  6. import * as navigation from '../src/utils/mp-webview'
  7. function handler(file: string, name: string, bindings: Record<string, any>) {
  8. const source = parse(readFileSync(new URL(`../src/${file}`, import.meta.url), 'utf8')).descriptor
  9. .scriptSetup!.content
  10. const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
  11. const fn = ast.statements.find(
  12. (node) => ts.isFunctionDeclaration(node) && node.name?.text === name
  13. )
  14. assert.ok(fn, `${name} must exist`)
  15. return new Function(...Object.keys(bindings), ts.transpile(fn.getText(ast)) + `;return ${name}`)(
  16. ...Object.values(bindings)
  17. )
  18. }
  19. test('mobile quote validates freight, deposit and validity bounds before either save path', () => {
  20. const form = {
  21. value: {
  22. quoteType: 'sales',
  23. supplierId: 's',
  24. customerId: 'c',
  25. contactId: 'p',
  26. ownerUserId: 'u',
  27. quoteDate: '2026-10-24',
  28. validityDays: 30,
  29. freight: 0,
  30. barrelDeposit: 0
  31. } as Record<string, any>
  32. }
  33. const validate = handler('views/crm/quote/components/QuoteFormPanel.vue', 'pageMissingMessage', {
  34. form,
  35. isPageVariant: { value: true },
  36. quoteMeta: { blockingMessage: () => '' },
  37. basicLabels: { value: {} }
  38. })
  39. assert.equal(validate(), '')
  40. for (const field of ['freight', 'barrelDeposit']) {
  41. for (const invalid of [-1, '-0.01', 'invalid', Infinity]) {
  42. form.value[field] = invalid
  43. assert.notEqual(validate(), '', `${field}=${invalid} must fail`)
  44. }
  45. for (const valid of [0, '0', 1.25, '', null]) {
  46. form.value[field] = valid
  47. assert.equal(validate(), '')
  48. }
  49. form.value[field] = 0
  50. }
  51. for (const invalid of [0, 366, Infinity, 'invalid']) {
  52. form.value.validityDays = invalid
  53. assert.notEqual(validate(), '', `validity=${invalid} must fail`)
  54. }
  55. for (const valid of [1, 365]) {
  56. form.value.validityDays = valid
  57. assert.equal(validate(), '')
  58. }
  59. })
  60. test('quote customer navigation preserves approval and contract origins using fixed mobile routes', () => {
  61. const nav = navigation as Record<string, any>
  62. assert.equal(typeof nav.mpQuoteCustomerLocation, 'function')
  63. assert.equal(typeof nav.mpRelatedQuoteReturnLocation, 'function')
  64. for (const origin of [
  65. {},
  66. { from: 'approve' },
  67. {
  68. from: 'contract',
  69. returnContractId: 'K-1',
  70. originCustomerId: 'C-0'
  71. }
  72. ]) {
  73. const location = nav.mpQuoteCustomerLocation({ id: 'Q-1', customerId: 'C-1' }, origin)
  74. assert.equal(location.path, '/m/crm/customer/C-1')
  75. assert.deepEqual(nav.mpRelatedQuoteReturnLocation(location.query), {
  76. path: '/m/crm/quote/Q-1',
  77. query: origin
  78. })
  79. }
  80. assert.equal(nav.mpQuoteCustomerLocation({ id: 'Q', customerId: '../bad' }, {}), null)
  81. assert.equal(nav.mpRelatedQuoteReturnLocation({ from: 'quote', returnQuoteId: '//evil' }), null)
  82. })
  83. test('quote customer click is guarded and customer back uses the quote return destination', () => {
  84. const targets: any[] = []
  85. let ready = false
  86. const goCustomer = handler('views/m/crm/quote/detail.vue', 'goCustomer', {
  87. navigationReady: () => ready,
  88. quote: { value: { id: 'Q', customerId: 'C' } },
  89. route: { query: { from: 'approve' } },
  90. router: { push: (v: any) => targets.push(v) },
  91. mpQuoteCustomerLocation: (navigation as any).mpQuoteCustomerLocation
  92. })
  93. goCustomer()
  94. assert.equal(targets.length, 0)
  95. ready = true
  96. goCustomer()
  97. assert.equal(targets[0].path, '/m/crm/customer/C')
  98. const destination = { path: '/m/crm/quote/Q', query: { from: 'approve' } }
  99. const back = handler('views/m/crm/customer/detail.vue', 'back', {
  100. relatedContractReturn: { value: null },
  101. relatedQuoteReturn: { value: destination },
  102. router: { replace: (v: any) => targets.push(v) },
  103. CUSTOMER_PATH: '/m/crm/customer'
  104. })
  105. back()
  106. assert.deepEqual(targets[1], destination)
  107. })