Files
shanxia2.0/web-admin/scripts/mobile-pl-list-details.test.ts
T

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