mobile-related-detail-route.test.ts 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  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 * as vue from 'vue'
  7. import { createRouter, createMemoryHistory, useRoute, useRouter } from 'vue-router'
  8. import * as navigation from '../src/utils/mp-webview'
  9. const flush = async () => { for (let i = 0; i < 20; i++) await Promise.resolve() }
  10. async function mount(kind: 'contract' | 'quote', embedded = false) {
  11. const filename = `../src/views/m/crm/${kind}/detail.vue`
  12. const script = parse(readFileSync(new URL(filename, import.meta.url), 'utf8')).descriptor.scriptSetup!.content
  13. const ast = ts.createSourceFile('detail.ts', script, ts.ScriptTarget.Latest, true)
  14. const bindings: Record<string, any> = {}
  15. for (const n of ast.statements.filter(ts.isImportDeclaration)) {
  16. if (n.importClause?.name) bindings[n.importClause.name.text] = () => undefined
  17. const named = n.importClause?.namedBindings
  18. if (named && ts.isNamedImports(named)) for (const e of named.elements) bindings[e.name.text] = () => undefined
  19. }
  20. const router = createRouter({ history: createMemoryHistory(), routes: [
  21. { path: '/m/crm/contract/:id', name: 'MpCrmContractDetail', component: {} },
  22. { path: '/m/crm/quote/:id', name: 'MpCrmQuoteDetail', component: {} },
  23. { path: '/m/crm/customer/:id', name: 'MpCrmCustomerDetail', component: {} },
  24. { path: '/m/pl/plan/:id', name: 'MpPlanDetail', component: {} }
  25. ] })
  26. await router.push(embedded ? '/m/pl/plan/P1' : `/m/crm/${kind}/A`)
  27. const reads: Array<{ id: string; resolve: (value: any) => void }> = []
  28. const props = vue.reactive({ embedded, recordId: 'E1', hideProductPrice: false })
  29. const transport = (id: string) => new Promise((resolve) => reads.push({ id, resolve }))
  30. Object.assign(bindings, Object.fromEntries(Object.entries(vue).filter(([k]) => /^[a-zA-Z_$][\w$]*$/.test(k) && k !== 'default')), navigation, {
  31. useRoute, useRouter,
  32. defineProps: () => props, withDefaults: (value: any) => value,
  33. useUserStore: () => ({ info: {} }),
  34. useDictStore: () => ({ dictMap: {}, getOptions: () => [], getLabel: () => '', getTagType: () => 'info' }),
  35. getContract: transport, getQuote: transport,
  36. fetchCategoryTree: async () => [], buildPathLabelMapFromTreeRoots: () => new Map(),
  37. listContractApproval: async () => ({ data: [] }), listApproval: async () => ({ data: [] }),
  38. listQuoteProduct: async () => ({ rows: [] }), enrichQuoteDetailProductMeta: async () => {},
  39. document: { querySelector: () => null }, window: { addEventListener() {}, removeEventListener() {}, clearTimeout() {} }
  40. })
  41. const statements = ast.statements.filter((s) => !ts.isImportDeclaration(s))
  42. const names = statements.flatMap((s) => ts.isFunctionDeclaration(s) ? [s.name!.text] : ts.isVariableStatement(s) ? s.declarationList.declarations.map((d) => d.name.getText(ast)) : [])
  43. const code = ts.transpile(statements.map((n) => n.getText(ast)).join('\n'), {target:ts.ScriptTarget.ESNext})
  44. const renderer = vue.createRenderer<any, any>({patchProp(){},insert(){},remove(){},createElement:()=>({}),createText:()=>({}),createComment:()=>({}),setText(){},setElementText(){},parentNode:()=>null,nextSibling:()=>null})
  45. let api: any
  46. const app = renderer.createApp({setup() {
  47. api = new Function(...Object.keys(bindings), `${code};return {${names.join(',')}}`)(...Object.values(bindings))
  48. return () => vue.h('div')
  49. }})
  50. app.use(router)
  51. app.mount({})
  52. api.onShellReady()
  53. await flush()
  54. return { api, reads, router, props, stop: () => app.unmount() }
  55. }
  56. for (const kind of ['contract', 'quote'] as const) for (const nextId of ['A', 'B']) {
  57. test(`${kind}离开到客户${nextId},不请求客户ID且迟到详情无权回写`, async () => {
  58. const h = await mount(kind)
  59. assert.deepEqual(h.reads.map((r) => r.id), ['A'])
  60. await h.router.push(`/m/crm/customer/${nextId}`)
  61. await flush()
  62. assert.deepEqual(h.reads.map((r) => r.id), ['A'], '离开watch不能发起外模块请求')
  63. void h.api.loadDetail()
  64. await flush()
  65. assert.deepEqual(h.reads.map((r) => r.id), ['A'])
  66. h.reads[0].resolve({ id: 'A', products: [], productList: [] })
  67. await flush()
  68. assert.equal(h.api[kind].value, null)
  69. assert.equal(h.api.currentRecordId(), '')
  70. await h.router.push(`/m/crm/${kind}/A`)
  71. await flush()
  72. assert.deepEqual(h.reads.map((r) => r.id), ['A', 'A'])
  73. h.stop()
  74. })
  75. }
  76. test('embedded合同只认recordId,宿主路径和ID变化不触发详情读取', async () => {
  77. const h = await mount('contract', true)
  78. await h.router.push('/m/crm/customer/B')
  79. await flush()
  80. assert.deepEqual(h.reads.map((r) => r.id), ['E1'])
  81. h.props.recordId = 'E2'
  82. await flush()
  83. assert.deepEqual(h.reads.map((r) => r.id), ['E1', 'E2'])
  84. h.reads[0].resolve({id:'E1'})
  85. await flush()
  86. assert.equal(h.api.contract.value, null)
  87. h.stop()
  88. })