255 lines
9.0 KiB
TypeScript
255 lines
9.0 KiB
TypeScript
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()
|
|
})
|