mobile-quote-customer-maintenance.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFileSync } from 'node:fs'
  4. import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
  5. import { ref, nextTick } from 'vue'
  6. import ts from 'typescript'
  7. const file = new URL('../src/views/crm/quote/components/QuoteFormPanel.vue', import.meta.url)
  8. function harness(fetchContacts: (input: any) => Promise<any> = async () => []) {
  9. const events: any[] = []
  10. const b: Record<string, any> = {
  11. form: ref({
  12. customerId: 'C1',
  13. customerName: '客户一',
  14. contactId: 'P1',
  15. contactName: '联系人一',
  16. products: [{ productId: 'product' }],
  17. freight: 12
  18. }),
  19. isPageVariant: ref(true),
  20. route: { fullPath: '/m/crm/quote/new' },
  21. customerShortName: ref('简称'),
  22. confirmedCustomerId: ref('C1'),
  23. contactOptions: ref([]),
  24. contactOptionsLoading: ref(false),
  25. contactOptionsError: ref(''),
  26. contactEmptyPrompted: ref(false),
  27. opportunityDisplayName: ref(''),
  28. mobileContactFormCustomerId: ref(''),
  29. mobileContactKeyword: ref(''),
  30. contactSelectorVisible: ref(false),
  31. mobileCustomerEditor: ref(null),
  32. mobileInvoiceCustomer: ref(null),
  33. customerSelectorVisible: ref(true),
  34. fetchContactsByCustomerId: fetchContacts,
  35. extractContactList: (res: any) => res?.data ?? res,
  36. ensureContactOption: () => {},
  37. ensureCustomerOption: (row: any) => events.push(['option', row]),
  38. updateQuoteTitleAndNo: () => events.push(['title']),
  39. loadMobileCustomers: async () => events.push(['reload']),
  40. basicLabels: ref({ contactName: '客户联系人' }),
  41. showFailToast: (message: string) => events.push(['error', message]),
  42. ElMessage: { warning: () => {} },
  43. nextTick,
  44. contactFormRef: ref({ open: () => events.push(['web-contact']) }),
  45. $modal: { msgWarning: () => {} },
  46. handleContactConfirm: (row: any) => {
  47. b.form.value.contactId = row.id
  48. b.form.value.contactName = row.contactName
  49. }
  50. }
  51. const names = [
  52. 'contactOptionsRequest',
  53. 'loadContactOptions',
  54. 'openMobileContactForm',
  55. 'openContactFormDialog',
  56. 'handleCustomerConfirm',
  57. 'openMobileContact',
  58. 'handleMobileContactSaved',
  59. 'handleMobileCustomerSaved',
  60. 'openMobileCustomerEditor',
  61. 'openMobileInvoice'
  62. ]
  63. const ast = ts.createSourceFile(
  64. 'panel.ts',
  65. parse(readFileSync(file, 'utf8')).descriptor.scriptSetup!.content,
  66. ts.ScriptTarget.Latest,
  67. true
  68. )
  69. const nodes = ast.statements.filter((n) =>
  70. ts.isFunctionDeclaration(n)
  71. ? names.includes(n.name?.text || '')
  72. : ts.isVariableStatement(n) &&
  73. names.includes(n.declarationList.declarations[0].name.getText(ast))
  74. )
  75. assert.equal(nodes.length, names.length, 'maintenance handlers must exist')
  76. const api = new Function(
  77. ...Object.keys(b),
  78. ts.transpile(nodes.map((n) => n.getText(ast)).join('\n')) + `;return {${names.join(',')}}`
  79. )(...Object.values(b))
  80. return { ...b, ...api, events }
  81. }
  82. test('customer chooser offers create, edit and invoice actions without selecting the row', () => {
  83. const t = parse(readFileSync(file, 'utf8')).descriptor.template!.content
  84. for (const action of [
  85. '@click="openMobileCustomerEditor()"',
  86. '@click.stop="openMobileCustomerEditor(item)"',
  87. '@click.stop="openMobileInvoice(item)"'
  88. ])
  89. assert.ok(t.includes(action), action)
  90. })
  91. test('empty contacts open mobile creation; existing primary contact is selected', async () => {
  92. for (const contacts of [
  93. [],
  94. [
  95. { id: 'P2', contactName: '普通' },
  96. { id: 'P3', contactName: '主要', isPrimary: 1 }
  97. ]
  98. ]) {
  99. const h = harness(async () => contacts)
  100. await h.handleCustomerConfirm({ id: 'C2', customerName: '客户二', shortName: '二' })
  101. assert.equal(h.mobileContactFormCustomerId.value, contacts.length ? '' : 'C2')
  102. assert.equal(h.form.value.contactId, contacts.length ? 'P3' : undefined)
  103. assert.equal(h.form.value.freight, 12)
  104. }
  105. })
  106. test('contact load failure permits retry and never opens create', async () => {
  107. let fail = true
  108. const h = harness(async () => {
  109. if (fail) throw new Error('网络失败')
  110. return []
  111. })
  112. await h.handleCustomerConfirm({ id: 'C2', customerName: '客户二' })
  113. assert.equal(h.mobileContactFormCustomerId.value, '')
  114. assert.ok(h.contactOptionsError.value)
  115. fail = false
  116. await h.openMobileContact()
  117. assert.equal(h.mobileContactFormCustomerId.value, 'C2')
  118. })
  119. for (const change of ['customer', 'route'])
  120. test(`late contact responses after ${change} change are discarded`, async () => {
  121. let resolve!: (value: any) => void
  122. const h = harness(
  123. () =>
  124. new Promise((r) => {
  125. resolve = r
  126. })
  127. )
  128. const pending = h.handleCustomerConfirm({ id: 'C2', customerName: '客户二' })
  129. if (change === 'customer') h.form.value.customerId = 'C3'
  130. else h.route.fullPath = '/m/crm/quote'
  131. resolve([])
  132. await pending
  133. assert.equal(h.mobileContactFormCustomerId.value, '')
  134. })
  135. test('customer editing refreshes labels but preserves quotation and contact', async () => {
  136. const h = harness()
  137. await h.handleMobileCustomerSaved({ id: 'C1', customerName: '新名称', shortName: '新简称' })
  138. assert.equal(h.form.value.customerName, '新名称')
  139. assert.equal(h.customerShortName.value, '新简称')
  140. assert.equal(h.form.value.contactId, 'P1')
  141. assert.equal(h.form.value.products[0].productId, 'product')
  142. assert.equal(h.form.value.freight, 12)
  143. assert.ok(h.events.some((e: any) => e[0] === 'reload'))
  144. })
  145. test('created contact fills current customer; stale customer events are ignored', async () => {
  146. const h = harness()
  147. h.mobileContactFormCustomerId.value = 'C1'
  148. await h.handleMobileContactSaved({ id: 'P2', customerId: 'C1', contactName: '新增联系人' })
  149. assert.equal(h.form.value.contactId, 'P2')
  150. await h.handleMobileContactSaved({ id: 'P3', customerId: 'OTHER', contactName: '错误联系人' })
  151. assert.equal(h.form.value.contactId, 'P2')
  152. })
  153. test('affected Vue templates compile', () => {
  154. for (const path of [
  155. 'views/crm/quote/components/QuoteFormPanel.vue',
  156. 'views/m/crm/customer/form.vue',
  157. 'views/m/crm/customer/ContactPanel.vue'
  158. ]) {
  159. const d = parse(readFileSync(new URL('../src/' + path, import.meta.url), 'utf8')).descriptor
  160. const s = compileScript(d, { id: path })
  161. assert.deepEqual(
  162. compileTemplate({
  163. source: d.template!.content,
  164. filename: path,
  165. id: path,
  166. compilerOptions: { bindingMetadata: s.bindings }
  167. }).errors,
  168. []
  169. )
  170. }
  171. })