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()), 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() })