| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import assert from 'node:assert/strict'
- import test from 'node:test'
- import { readFileSync } from 'node:fs'
- import { parse } from '@vue/compiler-sfc'
- import ts from 'typescript'
- import { computed, effectScope, nextTick, ref, watch } from 'vue'
- import { useMobilePlanListDetails } from '../src/views/m/pl/plan/mobileListDetails'
- import { formatBrandConstraintLabel } from '../src/utils/crm-product-brand'
- function harness() {
- const context = ref('account-A/query-A')
- const calls: any[] = []
- const scope = effectScope()
- const api = scope.run(() =>
- useMobilePlanListDetails({
- scope: () => context.value,
- load: (id: string) => new Promise((resolve, reject) => calls.push({ id, resolve, reject }))
- })
- )!
- return { api, context, calls, close: () => scope.stop() }
- }
- test('raw list without segment metadata waits for one lazy detail read and retains explicit empty metadata', async () => {
- const h = harness()
- const raw = {
- id: 'A',
- products: [{ id: 'p', aliasName: '根别名', brandName: '根品牌' }],
- deliveries: [{ products: [{ id: 'stop', planProductId: 'p' }] }]
- }
- assert.equal(h.calls.length, 0, 'list rendering must not prefetch detail')
- assert.equal(h.api.records[raw.id], undefined)
- const reading = h.api.ensure(raw.id)
- const sameRead = h.api.ensure(raw.id)
- assert.equal(sameRead, reading, 'restore and expansion share the pending promise')
- assert.equal(h.calls.length, 1)
- assert.equal(h.api.records.A.loading, true)
- assert.equal(h.api.records.A.plan, null, 'root data is not a completed segment snapshot')
- const meta = { aliasName: '段次别名', brandMode: 'any', brandName: '', brandId: '' }
- h.calls[0].resolve({
- data: {
- ...raw,
- deliveries: [{ products: [{ ...raw.deliveries[0].products[0], mobileProductMeta: meta }] }]
- }
- })
- await reading
- assert.deepEqual(h.api.records.A.plan?.deliveries?.[0].products[0].mobileProductMeta, meta)
- assert.equal(
- (raw.deliveries[0].products[0] as any).mobileProductMeta,
- undefined,
- 'list snapshot is not mutated'
- )
- await h.api.ensure(raw.id)
- assert.equal(h.calls.length, 1, 'reopening the same card reuses this query snapshot')
- h.close()
- })
- test('failed lazy detail remains an error and retry can replace it without showing root metadata', async () => {
- const h = harness()
- const first = h.api.ensure('A')
- h.calls[0].reject(new Error('offline'))
- await first
- assert.ok(h.api.records.A.error)
- assert.equal(h.api.records.A.plan, null)
- assert.equal(h.api.records.A.loading, false)
- const retry = h.api.ensure('A')
- h.calls[1].resolve({ id: 'A', deliveries: [] })
- await retry
- assert.equal(h.api.records.A.error, '')
- assert.equal(h.api.records.A.plan?.id, 'A')
- h.close()
- })
- for (const invalidation of ['identity/query', 'reset', 'unmount']) {
- test(`lazy detail ignores success and failure after ${invalidation}`, async () => {
- const h = harness()
- const first = h.api.ensure('A'),
- second = h.api.ensure('B')
- if (invalidation === 'identity/query') h.context.value = 'account-B/query-B'
- else if (invalidation === 'reset') h.api.reset()
- else h.close()
- h.calls[0].resolve({ id: 'A', deliveries: [] })
- h.calls[1].reject(new Error('late error'))
- await Promise.all([first, second])
- assert.deepEqual(Object.keys(h.api.records), [])
- if (invalidation !== 'unmount') h.close()
- })
- }
- test('mismatched detail ID is never used as the expanded card snapshot', async () => {
- const h = harness()
- const reading = h.api.ensure('A')
- h.calls[0].resolve({ id: 'B' })
- await reading
- assert.equal(h.api.records.A.plan, null)
- assert.ok(h.api.records.A.error)
- h.close()
- })
- test('card names join exact plan product IDs without replacing detail quantity or segment metadata', async () => {
- const h = harness()
- const listed = {
- id: 'A',
- products: [
- { id: 'first', productId: 'same', productName: '标准甲' },
- { id: 'second', productId: 'same', productName: '标准乙' }
- ]
- }
- const metadata = { aliasName: '段次别名', brandMode: 'any', brandName: '', brandId: '' }
- const detail = {
- id: 'A',
- products: [
- { id: 'second', productId: 'same', productName: '别名乙', quantity: 2 },
- { id: 'first', productId: 'same', productName: '别名甲', quantity: 1 },
- { id: 'unmatched', productId: 'same', productName: '独立行', quantity: 3 }
- ],
- deliveries: [{ products: [{ planProductId: 'second', mobileProductMeta: metadata }] }]
- }
- const reading = h.api.ensure('A')
- h.calls[0].resolve(detail)
- await reading
- const card = h.api.cardPlan(listed as any)
- assert.deepEqual(
- card.products?.map((p: any) => [p.standardProductName, p.productName, p.quantity]),
- [
- ['标准乙', '别名乙', 2],
- ['标准甲', '别名甲', 1],
- [undefined, '独立行', 3]
- ]
- )
- assert.deepEqual(card.deliveries?.[0].products[0].mobileProductMeta, metadata)
- assert.equal((detail.products[0] as any).standardProductName, undefined)
- h.close()
- })
- test('the actual list expansion requests mobile detail for only that card and uses its segment snapshot', async () => {
- const calls: any[] = []
- const raw = {
- id: 'A',
- products: [{ id: 'p', productName: '标准产品', aliasName: '根别名' }],
- deliveries: [{ products: [{ id: 'stop', planProductId: 'p' }] }]
- }
- const plans = ref([raw, { id: 'B' }])
- const ast = ts.createSourceFile(
- 'list.ts',
- parse(readFileSync(new URL('../src/views/m/pl/plan/index.vue', import.meta.url), 'utf8'))
- .descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const code = ast.statements
- .filter(
- (node) =>
- (ts.isVariableStatement(node) &&
- node.declarationList.declarations.some((d) =>
- ['planDetails', 'planRows'].includes(d.name.getText(ast))
- )) ||
- (ts.isFunctionDeclaration(node) && node.name?.text === 'toggleCardExpand') ||
- (ts.isExpressionStatement(node) &&
- ts.isCallExpression(node.expression) &&
- node.expression.expression.getText(ast) === 'watch' &&
- node.expression.arguments[0]?.getText(ast) === 'expandedIds')
- )
- .map((node) => node.getText(ast))
- .join('\n')
- const cardAst = ts.createSourceFile(
- 'card.ts',
- readFileSync(new URL('../src/utils/mp-pl-plan-card.ts', import.meta.url), 'utf8'),
- ts.ScriptTarget.Latest,
- true
- )
- const cardCode = cardAst.statements
- .filter(
- (node) =>
- ts.isFunctionDeclaration(node) &&
- [
- 'productSummary',
- 'formatStopProductLine',
- 'resolveLinkedPlanProduct',
- 'formatQty',
- 'resolveUnitLabel'
- ].includes(node.name!.text)
- )
- .map((node) => node.getText(cardAst))
- .join('\n')
- const cardEnv = {
- formatBrandConstraintLabel,
- resolveProductHazardFlags: () => ({}),
- getPlanProductActualQuantity: () => '0',
- resolveMpPackageSpecLabel: () => '—',
- resolveMpProductUnitLabel: () => '—'
- }
- const buildPlanCard = new Function(
- ...Object.keys(cardEnv),
- `${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 : ''})`
- )(...Object.values(cardEnv))
- const env = {
- computed,
- watch,
- plans,
- expandedIds: ref(new Set<string>()),
- useMobilePlanListDetails,
- requestKey: () => 'session/query',
- planApi: {
- get: (id: string, mobile: boolean) =>
- new Promise((resolve) => calls.push({ id, mobile, resolve }))
- },
- buildPlanCard,
- canArrangeTransport: () => false,
- arrangeTransportLabel: () => ''
- }
- const scope = effectScope()
- const api = scope.run(() =>
- new Function(
- ...Object.keys(env),
- `${ts.transpile(code, { target: ts.ScriptTarget.ESNext })};return {planDetails,planRows,toggleCardExpand}`
- )(...Object.values(env))
- )
- assert.equal(calls.length, 0)
- assert.equal(api.planRows.value[0].card.product, '标准产品')
- api.toggleCardExpand('A')
- await nextTick()
- assert.deepEqual(
- calls.map(({ id, mobile }) => [id, mobile]),
- [['A', true]]
- )
- const mobileProductMeta = { aliasName: '送货别名', brandMode: 'any', brandName: '', brandId: '' }
- calls[0].resolve({
- ...raw,
- products: [{ ...raw.products[0], productName: '根别名' }],
- deliveries: [{ products: [{ ...raw.deliveries[0].products[0], mobileProductMeta }] }]
- })
- await nextTick()
- assert.equal(
- api.planRows.value[0].card.product,
- '标准产品',
- 'detail alias must not replace preview name'
- )
- assert.equal(
- api.planRows.value[0].card.stopName,
- '标准产品',
- 'expanded title uses the standard product name'
- )
- assert.deepEqual(
- api.planRows.value[0].card.deliveries[0].products[0].mobileProductMeta,
- mobileProductMeta
- )
- assert.equal((plans.value[0] as any).deliveries[0].products[0].mobileProductMeta, undefined)
- api.toggleCardExpand('A')
- await nextTick()
- api.toggleCardExpand('A')
- await nextTick()
- assert.equal(calls.length, 1)
- scope.stop()
- })
|