mobile-plan-contract-embedded.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 { compile, createSSRApp } from 'vue'
  6. import { renderToString } from 'vue/server-renderer'
  7. import ts from 'typescript'
  8. const path = new URL('../src/views/m/crm/contract/detail.vue', import.meta.url)
  9. const d = parse(readFileSync(path, 'utf8')).descriptor
  10. const ast = ts.createSourceFile('detail.ts', d.scriptSetup!.content, ts.ScriptTarget.Latest, true)
  11. function fn(name: string, bindings: any) {
  12. const node = ast.statements.find((n) => ts.isFunctionDeclaration(n) && n.name?.text === name)!
  13. return new Function(
  14. ...Object.keys(bindings),
  15. ts.transpile(node.getText(ast)) + `;return ${name}`
  16. )(...Object.values(bindings))
  17. }
  18. test('embedded identity comes only from recordId and normal identity stays route based', () => {
  19. const props = { embedded: true, recordId: 'contract' }
  20. const route = { params: { id: 'plan' } }
  21. const current = fn('currentRecordId', { props, route })
  22. assert.equal(current(), 'contract')
  23. props.embedded = false
  24. assert.equal(current(), 'plan')
  25. })
  26. test('embedded route actions never navigate or write', async () => {
  27. let calls = 0
  28. await fn('runRouteAction', {
  29. props: { embedded: true },
  30. route: { query: { action: 'delete' } },
  31. captureOperation: () => {
  32. calls++
  33. return {}
  34. },
  35. router: {
  36. replace: async () => {
  37. calls++
  38. }
  39. },
  40. isCurrentOperation: () => true,
  41. runAction: () => calls++
  42. })()
  43. assert.equal(calls, 0)
  44. })
  45. test('embedded template reuses complete detail, readonly attachments and masks all numeric pricing regions', async () => {
  46. compileScript(d, { id: 'embedded' })
  47. assert.deepEqual(
  48. compileTemplate({ source: d.template!.content, filename: path.pathname, id: 'embedded' })
  49. .errors,
  50. []
  51. )
  52. const template = d.template!.content
  53. assert.match(template, /:editable="!props.embedded && !isApproveEntry"/)
  54. assert.match(template, /priceTiers/)
  55. for (const id of ['amount', 'products']) {
  56. const fragment = template.slice(
  57. template.indexOf(`<section id="contract-sec-${id}"`),
  58. template.indexOf('</section>', template.indexOf(`<section id="contract-sec-${id}"`)) + 10
  59. )
  60. const bindings = {
  61. props: { embedded: true, hideProductPrice: true },
  62. contract: { amount: 9876, freight: 9876, barrelDeposit: 9876 },
  63. products: [{ productName: '产品', price: 9876, priceTiers: [{ price: 9876 }] }],
  64. formatContractMoney: () => '9876',
  65. productMoney: () => '9876',
  66. displayCategory: () => '',
  67. unitLabel: () => '',
  68. formatBrandConstraintLabel: () => '',
  69. isFrameworkContractType: () => true
  70. }
  71. const render = compile(fragment)
  72. const html = await renderToString(
  73. createSSRApp({ render: () => render.call(bindings, bindings, []) })
  74. )
  75. assert.doesNotMatch(html, /9876/)
  76. }
  77. })
  78. test('embedded real Vue identity watcher ignores plan route changes and rejects old request after switch or unmount', async () => {
  79. const { reactive, ref, watch, nextTick, effectScope } = await import('vue')
  80. const props = reactive({ embedded: true, recordId: 'A' })
  81. const route = reactive({ params: { id: 'plan' }, query: {} })
  82. let resolveOld: (v: any) => void = () => {}
  83. const old = new Promise((r) => {
  84. resolveOld = r
  85. })
  86. const hooks: Array<() => void> = []
  87. const names = ['currentRecordId', 'isCurrentLoad', 'loadDetail', 'resetRecord', 'runRouteAction']
  88. const declarations = ast.statements
  89. .filter((n) => ts.isFunctionDeclaration(n) && names.includes(n.name?.text || ''))
  90. .map((n) => n.getText(ast))
  91. const lifecycle = ast.statements
  92. .filter(
  93. (n) =>
  94. ts.isExpressionStatement(n) &&
  95. ((n.getText(ast).startsWith('watch(') &&
  96. n.getText(ast).includes('() => currentRecordId()')) ||
  97. n.getText(ast).startsWith('onBeforeUnmount('))
  98. )
  99. .map((n) => n.getText(ast))
  100. const calls: string[] = []
  101. const bindings = {
  102. props,
  103. route,
  104. watch,
  105. onBeforeUnmount: (cb: () => void) => hooks.push(cb),
  106. contract: ref<any>(null),
  107. products: ref<any[]>([]),
  108. approvalList: ref<any[]>([]),
  109. categoryPaths: ref(new Map()),
  110. errorText: ref(''),
  111. loading: ref(false),
  112. closeRecordActions: () => {},
  113. loadCategoryPaths: async () => {},
  114. getContract: async (id: string) => {
  115. calls.push(id)
  116. return id === 'A' ? old : { id, products: [] }
  117. },
  118. listContractApproval: async () => ({ data: [] }),
  119. getQuote: async () => ({})
  120. }
  121. const scope = effectScope()
  122. const run = scope.run(() =>
  123. new Function(
  124. ...Object.keys(bindings),
  125. 'let detailRequestSeq=0;let shellReady=true;let disposed=false;' +
  126. ts.transpile([...declarations, ...lifecycle].join('\n')) +
  127. ';return {loadDetail}'
  128. )(...Object.values(bindings))
  129. )
  130. const pending = run.loadDetail()
  131. route.params.id = 'other-plan'
  132. await nextTick()
  133. assert.deepEqual(calls, ['A'])
  134. props.recordId = 'B'
  135. await nextTick()
  136. await new Promise((r) => setTimeout(r, 0))
  137. assert.equal(bindings.contract.value?.id, 'B')
  138. resolveOld({ id: 'A', products: [{ id: 'stale' }] })
  139. await pending
  140. assert.equal(bindings.contract.value?.id, 'B')
  141. hooks.forEach((h) => h())
  142. assert.equal(bindings.contract.value, null)
  143. scope.stop()
  144. })