mobile-pl-list-details.test.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 { computed, effectScope, nextTick, ref, watch } from 'vue'
  7. import { useMobilePlanListDetails } from '../src/views/m/pl/plan/mobileListDetails'
  8. import { formatBrandConstraintLabel } from '../src/utils/crm-product-brand'
  9. function harness() {
  10. const context = ref('account-A/query-A')
  11. const calls: any[] = []
  12. const scope = effectScope()
  13. const api = scope.run(() =>
  14. useMobilePlanListDetails({
  15. scope: () => context.value,
  16. load: (id: string) => new Promise((resolve, reject) => calls.push({ id, resolve, reject }))
  17. })
  18. )!
  19. return { api, context, calls, close: () => scope.stop() }
  20. }
  21. test('raw list without segment metadata waits for one lazy detail read and retains explicit empty metadata', async () => {
  22. const h = harness()
  23. const raw = {
  24. id: 'A',
  25. products: [{ id: 'p', aliasName: '根别名', brandName: '根品牌' }],
  26. deliveries: [{ products: [{ id: 'stop', planProductId: 'p' }] }]
  27. }
  28. assert.equal(h.calls.length, 0, 'list rendering must not prefetch detail')
  29. assert.equal(h.api.records[raw.id], undefined)
  30. const reading = h.api.ensure(raw.id)
  31. const sameRead = h.api.ensure(raw.id)
  32. assert.equal(sameRead, reading, 'restore and expansion share the pending promise')
  33. assert.equal(h.calls.length, 1)
  34. assert.equal(h.api.records.A.loading, true)
  35. assert.equal(h.api.records.A.plan, null, 'root data is not a completed segment snapshot')
  36. const meta = { aliasName: '段次别名', brandMode: 'any', brandName: '', brandId: '' }
  37. h.calls[0].resolve({
  38. data: {
  39. ...raw,
  40. deliveries: [{ products: [{ ...raw.deliveries[0].products[0], mobileProductMeta: meta }] }]
  41. }
  42. })
  43. await reading
  44. assert.deepEqual(h.api.records.A.plan?.deliveries?.[0].products[0].mobileProductMeta, meta)
  45. assert.equal(
  46. (raw.deliveries[0].products[0] as any).mobileProductMeta,
  47. undefined,
  48. 'list snapshot is not mutated'
  49. )
  50. await h.api.ensure(raw.id)
  51. assert.equal(h.calls.length, 1, 'reopening the same card reuses this query snapshot')
  52. h.close()
  53. })
  54. test('failed lazy detail remains an error and retry can replace it without showing root metadata', async () => {
  55. const h = harness()
  56. const first = h.api.ensure('A')
  57. h.calls[0].reject(new Error('offline'))
  58. await first
  59. assert.ok(h.api.records.A.error)
  60. assert.equal(h.api.records.A.plan, null)
  61. assert.equal(h.api.records.A.loading, false)
  62. const retry = h.api.ensure('A')
  63. h.calls[1].resolve({ id: 'A', deliveries: [] })
  64. await retry
  65. assert.equal(h.api.records.A.error, '')
  66. assert.equal(h.api.records.A.plan?.id, 'A')
  67. h.close()
  68. })
  69. for (const invalidation of ['identity/query', 'reset', 'unmount']) {
  70. test(`lazy detail ignores success and failure after ${invalidation}`, async () => {
  71. const h = harness()
  72. const first = h.api.ensure('A'),
  73. second = h.api.ensure('B')
  74. if (invalidation === 'identity/query') h.context.value = 'account-B/query-B'
  75. else if (invalidation === 'reset') h.api.reset()
  76. else h.close()
  77. h.calls[0].resolve({ id: 'A', deliveries: [] })
  78. h.calls[1].reject(new Error('late error'))
  79. await Promise.all([first, second])
  80. assert.deepEqual(Object.keys(h.api.records), [])
  81. if (invalidation !== 'unmount') h.close()
  82. })
  83. }
  84. test('mismatched detail ID is never used as the expanded card snapshot', async () => {
  85. const h = harness()
  86. const reading = h.api.ensure('A')
  87. h.calls[0].resolve({ id: 'B' })
  88. await reading
  89. assert.equal(h.api.records.A.plan, null)
  90. assert.ok(h.api.records.A.error)
  91. h.close()
  92. })
  93. test('card names join exact plan product IDs without replacing detail quantity or segment metadata', async () => {
  94. const h = harness()
  95. const listed = {
  96. id: 'A',
  97. products: [
  98. { id: 'first', productId: 'same', productName: '标准甲' },
  99. { id: 'second', productId: 'same', productName: '标准乙' }
  100. ]
  101. }
  102. const metadata = { aliasName: '段次别名', brandMode: 'any', brandName: '', brandId: '' }
  103. const detail = {
  104. id: 'A',
  105. products: [
  106. { id: 'second', productId: 'same', productName: '别名乙', quantity: 2 },
  107. { id: 'first', productId: 'same', productName: '别名甲', quantity: 1 },
  108. { id: 'unmatched', productId: 'same', productName: '独立行', quantity: 3 }
  109. ],
  110. deliveries: [{ products: [{ planProductId: 'second', mobileProductMeta: metadata }] }]
  111. }
  112. const reading = h.api.ensure('A')
  113. h.calls[0].resolve(detail)
  114. await reading
  115. const card = h.api.cardPlan(listed as any)
  116. assert.deepEqual(
  117. card.products?.map((p: any) => [p.standardProductName, p.productName, p.quantity]),
  118. [
  119. ['标准乙', '别名乙', 2],
  120. ['标准甲', '别名甲', 1],
  121. [undefined, '独立行', 3]
  122. ]
  123. )
  124. assert.deepEqual(card.deliveries?.[0].products[0].mobileProductMeta, metadata)
  125. assert.equal((detail.products[0] as any).standardProductName, undefined)
  126. h.close()
  127. })
  128. test('the actual list expansion requests mobile detail for only that card and uses its segment snapshot', async () => {
  129. const calls: any[] = []
  130. const raw = {
  131. id: 'A',
  132. products: [{ id: 'p', productName: '标准产品', aliasName: '根别名' }],
  133. deliveries: [{ products: [{ id: 'stop', planProductId: 'p' }] }]
  134. }
  135. const plans = ref([raw, { id: 'B' }])
  136. const ast = ts.createSourceFile(
  137. 'list.ts',
  138. parse(readFileSync(new URL('../src/views/m/pl/plan/index.vue', import.meta.url), 'utf8'))
  139. .descriptor.scriptSetup!.content,
  140. ts.ScriptTarget.Latest,
  141. true
  142. )
  143. const code = ast.statements
  144. .filter(
  145. (node) =>
  146. (ts.isVariableStatement(node) &&
  147. node.declarationList.declarations.some((d) =>
  148. ['planDetails', 'planRows'].includes(d.name.getText(ast))
  149. )) ||
  150. (ts.isFunctionDeclaration(node) && node.name?.text === 'toggleCardExpand') ||
  151. (ts.isExpressionStatement(node) &&
  152. ts.isCallExpression(node.expression) &&
  153. node.expression.expression.getText(ast) === 'watch' &&
  154. node.expression.arguments[0]?.getText(ast) === 'expandedIds')
  155. )
  156. .map((node) => node.getText(ast))
  157. .join('\n')
  158. const cardAst = ts.createSourceFile(
  159. 'card.ts',
  160. readFileSync(new URL('../src/utils/mp-pl-plan-card.ts', import.meta.url), 'utf8'),
  161. ts.ScriptTarget.Latest,
  162. true
  163. )
  164. const cardCode = cardAst.statements
  165. .filter(
  166. (node) =>
  167. ts.isFunctionDeclaration(node) &&
  168. [
  169. 'productSummary',
  170. 'formatStopProductLine',
  171. 'resolveLinkedPlanProduct',
  172. 'formatQty',
  173. 'resolveUnitLabel'
  174. ].includes(node.name!.text)
  175. )
  176. .map((node) => node.getText(cardAst))
  177. .join('\n')
  178. const cardEnv = {
  179. formatBrandConstraintLabel,
  180. resolveProductHazardFlags: () => ({}),
  181. getPlanProductActualQuantity: () => '0',
  182. resolveMpPackageSpecLabel: () => '—',
  183. resolveMpProductUnitLabel: () => '—'
  184. }
  185. const buildPlanCard = new Function(
  186. ...Object.keys(cardEnv),
  187. `${ts.transpile(cardCode, { target: ts.ScriptTarget.ESNext })};return (plan) => ({...plan, product: productSummary(plan), stopName: plan.deliveries?.[0]?.products[0] ? formatStopProductLine(plan, {kind:'delivery'}, plan.deliveries[0].products[0]).name : ''})`
  188. )(...Object.values(cardEnv))
  189. const env = {
  190. computed,
  191. watch,
  192. plans,
  193. expandedIds: ref(new Set<string>()),
  194. useMobilePlanListDetails,
  195. requestKey: () => 'session/query',
  196. planApi: {
  197. get: (id: string, mobile: boolean) =>
  198. new Promise((resolve) => calls.push({ id, mobile, resolve }))
  199. },
  200. buildPlanCard,
  201. canArrangeTransport: () => false,
  202. arrangeTransportLabel: () => ''
  203. }
  204. const scope = effectScope()
  205. const api = scope.run(() =>
  206. new Function(
  207. ...Object.keys(env),
  208. `${ts.transpile(code, { target: ts.ScriptTarget.ESNext })};return {planDetails,planRows,toggleCardExpand}`
  209. )(...Object.values(env))
  210. )
  211. assert.equal(calls.length, 0)
  212. assert.equal(api.planRows.value[0].card.product, '标准产品')
  213. api.toggleCardExpand('A')
  214. await nextTick()
  215. assert.deepEqual(
  216. calls.map(({ id, mobile }) => [id, mobile]),
  217. [['A', true]]
  218. )
  219. const mobileProductMeta = { aliasName: '送货别名', brandMode: 'any', brandName: '', brandId: '' }
  220. calls[0].resolve({
  221. ...raw,
  222. products: [{ ...raw.products[0], productName: '根别名' }],
  223. deliveries: [{ products: [{ ...raw.deliveries[0].products[0], mobileProductMeta }] }]
  224. })
  225. await nextTick()
  226. assert.equal(
  227. api.planRows.value[0].card.product,
  228. '标准产品',
  229. 'detail alias must not replace preview name'
  230. )
  231. assert.equal(
  232. api.planRows.value[0].card.stopName,
  233. '标准产品',
  234. 'expanded title uses the standard product name'
  235. )
  236. assert.deepEqual(
  237. api.planRows.value[0].card.deliveries[0].products[0].mobileProductMeta,
  238. mobileProductMeta
  239. )
  240. assert.equal((plans.value[0] as any).deliveries[0].products[0].mobileProductMeta, undefined)
  241. api.toggleCardExpand('A')
  242. await nextTick()
  243. api.toggleCardExpand('A')
  244. await nextTick()
  245. assert.equal(calls.length, 1)
  246. scope.stop()
  247. })