Files
shanxia2.0/web-admin/scripts/mobile-pl-plan-detail.test.ts
T

428 lines
15 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
actualQuantity,
matchStopLinks,
stopFeedbacks,
freightFields,
loadMobileDetailReads
} from '../src/views/m/pl/plan/mobileDetailDisplay'
test('actual quantities use segment actuals including zero, never estimates', () => {
assert.equal(
actualQuantity({ inboundQty: 33.97, outboundQty: 34, planQty: 30 } as any, 'load'),
'33.97'
)
assert.equal(actualQuantity({ inboundQty: 33.97, outboundQty: 34 } as any, 'delivery'), '34')
assert.equal(actualQuantity({ inboundQty: 0 } as any, 'load'), '0')
assert.equal(actualQuantity({ planQty: 30 } as any, 'load'), '—')
})
test('exact stop identity, legacy only plan product identity, never catalog identity', () => {
const stop = {
id: 'a',
products: [
{ id: 'line1', productId: 'same', planProductId: 'p1' },
{ id: 'line2', productId: 'same', planProductId: 'p2' }
]
}
const links = [
{ stopType: 'delivery', stopId: 'b', planProductId: 'p1' },
{ stopType: 'load', stopId: 'a', planProductId: 'p2' },
{ planProductId: 'p1' },
{ productId: 'same' }
]
assert.deepEqual(matchStopLinks(links, stop, 'load'), [links[1], links[2]])
assert.equal(
stopFeedbacks(
{
driverStopFeedbacks: [
{ stopType: 'delivery', stopId: 'b' },
{ stopType: 'load', stopId: 'a' }
]
} as any,
stop,
'load'
).length,
1
)
})
test('legacy driver feedback remains distinct and keeps zero', () => {
const f = stopFeedbacks(
{ feedbackQty: 0, feedbackImages: '["a.pdf"]', feedbackRemark: '旧记录' } as any,
{},
'load'
)
assert.equal(f[0].quantity, 0)
assert.deepEqual(f[0].attachmentUrls, ['a.pdf'])
assert.equal(stopFeedbacks({ feedbackQty: 3 } as any, {}, 'delivery').length, 0)
})
test('eight freight sources, trip hides quantity and unit price, no freight blanks', () => {
const fields = freightFields(
{ hasFreight: 1, freightAmount: 500, products: [{ actualQty: 34 }] } as any,
{ freightType: 'by_trip', freightAmount: 999, transportQty: 0, hasInvoice: 0 } as any,
{ invoiceAmount: 10 } as any
)
assert.equal(fields.length, 8)
assert.equal(fields[3].value, '—')
assert.equal(fields[4].value, '—')
assert.equal(fields[5].value, '500.00')
assert.equal(fields[6].value, '否')
assert.ok(
freightFields({ hasFreight: 0 } as any, {} as any, null)
.slice(1)
.every((f) => f.value === '—')
)
assert.equal(
freightFields({ hasFreight: 1 } as any, { transportQty: 0 } as any, null)[3].value,
'0'
)
})
test('partial failures are visible and retry performs only declared reads; masked prices untouched', async () => {
let failed = true
const plan: any = {
id: 'p',
transportId: 't',
transportStatus: 'arranged',
products: [{ inboundPrice: null }]
}
const reads: any = {
transport: async () => {
if (failed) throw Error()
return { id: 't' }
},
freight: async () => null,
arrange: async () => ({ inventoryRecords: [] }),
inventory: async () => null,
related: async () => null,
io: async () => null,
vehicle: async () => null
}
const first = await loadMobileDetailReads(plan, reads)
assert.deepEqual(first.errors, ['运输反馈'])
failed = false
const next = await loadMobileDetailReads(plan, reads)
assert.deepEqual(next.errors, [])
assert.equal(plan.products[0].inboundPrice, null)
})
test('production page controller rejects late results from another route', async () => {
const { readFileSync } = await import('node:fs')
const { parse } = await import('@vue/compiler-sfc')
const ts = await import('typescript')
const { ref } = await import('vue')
const source = parse(
readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8')
).descriptor.scriptSetup!.content
const ast = ts.createSourceFile('page.ts', source, ts.ScriptTarget.Latest, true)
const fn = ast.statements.find(
(n) => ts.isFunctionDeclaration(n) && n.name?.text === 'loadDetail'
)!
let release!: (value: any) => void
const older = new Promise((resolve) => {
release = resolve
})
const plan = ref<any>(null),
recordId = ref('older'),
readDetail = ref<any>(null),
partialErrors = ref([])
const planReads: unknown[][] = []
const bindings: any = {
plan,
recordId,
readDetail,
partialErrors,
loading: ref(false),
errorText: ref(''),
siteRecords: ref([]),
inventoryRecords: ref([]),
planApi: {
get: async (id: string, includeMobileProductMeta?: boolean) => {
planReads.push([id, includeMobileProductMeta])
return id === 'older' ? older : { id, planType: 'transport', products: [] }
}
},
loadArrangeActions: async () => {},
resetArrangeActions: () => {},
loadMobileDetailReads: async () => ({ errors: [], transport: null }),
transportApi: {},
freightApi: {},
inventoryApi: {},
inboundOrderApi: {},
outboundOrderApi: {},
vehicleApi: {},
personnelApi: {}
}
const load = new Function(
...Object.keys(bindings),
`let shellReady=true, disposed=false, loadVersion=0; function invalidateRecord(){loadVersion++;plan.value=null;readDetail.value=null} ${ts.transpile(fn.getText(ast), { target: ts.ScriptTarget.ESNext })};return loadDetail`
)(...Object.values(bindings))
const first = load()
recordId.value = 'newer'
await load()
release({ id: 'older' })
await first
assert.equal(plan.value.id, 'newer')
assert.deepEqual(planReads, [
['older', true],
['newer', true]
])
})
test('prefetched arrange and historical snapshots avoid redundant reads and retain stop attachments', async () => {
const snapshot = { licensePlate: '历史车号', personnelList: [{ personnelName: '历史司机' }] }
const feedback: any = {
driverStopFeedbacks: [
{
stopType: 'load',
stopId: 'a',
attachmentUrls: ['load.pdf'],
products: [
{ stopProductId: 'r1', planProductId: 'p1', actualQty: 1 },
{ stopProductId: 'r2', planProductId: 'p2', actualQty: 2 }
]
},
{ stopType: 'delivery', stopId: 'b', attachmentUrls: ['delivery.mp4'] }
]
}
let forbidden = 0
const reads: any = {
transport: async () => feedback,
freight: async () => null,
arrange: async () => {
forbidden++
throw Error()
},
vehicle: async () => {
forbidden++
throw Error()
},
inventory: async () => ({ planNo: 'CRK' }),
related: async () => ({ weighing: { netWeight: 34 } })
}
const detail = await loadMobileDetailReads(
{
id: 'p',
planType: 'transport',
transportStatus: 'arranged',
transportId: 't',
vehicleId: 'v',
resourceSnapshot: snapshot
} as any,
reads,
{
inventoryRecords: [
{ stopType: 'load', stopId: 'a', dbInventoryId: 'db', planProductId: 'p1' }
]
}
)
assert.equal(forbidden, 0)
assert.equal(detail.snapshot, snapshot)
const load = stopFeedbacks(detail.transport, { id: 'a' }, 'load')
assert.deepEqual(load[0].attachmentUrls, ['load.pdf'])
assert.deepEqual(
load[0].products?.map((p) => p.stopProductId),
['r1', 'r2']
)
assert.equal(detail.dibangRows[0].weighing.netWeight, 34)
})
test('segment metadata overrides parent values including explicit empty values', async () => {
const mod: any = await import('../src/views/m/pl/plan/mobileDetailDisplay')
assert.equal(typeof mod.stopProductMetadata, 'function')
const parent: any = [{ id: 'p', aliasName: '装货别名', brandName: '装货品牌' }]
const row: any = {
planProductId: 'p',
mobileProductMeta: { aliasName: '送货别名', brandMode: '', brandId: '', brandName: '' }
}
assert.deepEqual(mod.stopProductMetadata(row, parent), row.mobileProductMeta)
assert.equal(
mod.stopProductMetadata({ ...row, mobileProductMeta: { aliasName: '', brandName: '' } }, parent)
.aliasName,
''
)
})
test('legacy vehicle personnel certificate failures stay visible and retryable', async () => {
let failed = true
const reads: any = {
vehicle: async () => ({
data: {
licensePlate: '旧车',
personnelList: [{ personnelId: 'driver', personnelName: '司机' }]
}
}),
personnel: async () => {
if (failed) throw Error('cert failed')
return { certList: [{ certName: '从业证', attachmentUrls: 'cert.pdf' }] }
}
}
const plan: any = { id: 'p', planType: 'transport', vehicleId: 'v' }
const result = await loadMobileDetailReads(plan, reads)
assert.deepEqual(result.errors, ['人员证书'])
assert.equal(result.vehicle.personnelList[0].personnelName, '司机')
assert.equal(result.vehicle.personnelList[0].certLoadFailed, true)
failed = false
const retry = await loadMobileDetailReads(plan, reads)
assert.deepEqual(retry.errors, [])
assert.equal(retry.vehicle.personnelList[0].certList[0].attachmentUrls, 'cert.pdf')
})
async function statusHarness(options: { failRead?: boolean; write?: () => Promise<any> } = {}) {
const { readFileSync } = await import('node:fs')
const { parse } = await import('@vue/compiler-sfc')
const ts = await import('typescript')
const { ref } = await import('vue')
const source = parse(
readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8')
).descriptor.scriptSetup!.content
const ast = ts.createSourceFile('page.ts', source, ts.ScriptTarget.Latest, true)
const names = ['invalidateRecord', 'loadDetail', 'changePlanStatus']
const code = names
.map((name) =>
ast.statements.find((n) => ts.isFunctionDeclaration(n) && n.name?.text === name)!.getText(ast)
)
.join('\n')
const calls: any[] = []
const full: any = {
id: 'p',
products: [{ id: 'product' }],
load: { products: [{ mobileProductMeta: { aliasName: '分段别名', brandName: '' } }] }
}
const b: any = {
recordId: ref('p'),
plan: ref(full),
readDetail: ref(null),
partialErrors: ref([]),
errorText: ref(''),
loading: ref(false),
acting: ref(false),
deliveryIndex: ref(0),
activeTab: ref('base'),
moreOpen: ref(false),
voidOpen: ref(false),
voidReason: ref(''),
vehicleOpen: ref(false),
siteRecords: ref([]),
inventoryRecords: ref([]),
resetArrangeActions: () => {},
loadArrangeActions: async () => {},
showCrmConfirmDialog: async () => {},
loadMobileDetailReads: async () => ({ errors: [], transport: { id: 'transport' } }),
transportApi: {},
freightApi: {},
inventoryApi: {},
inboundOrderApi: {},
outboundOrderApi: {},
vehicleApi: {},
personnelApi: {},
ElMessage: {
success: (s: string) => calls.push(['success', s]),
warning: (s: string) => calls.push(['warning', s]),
error: (s: string) => calls.push(['error', s])
}
}
const write = async () => {
calls.push(['write'])
return options.write ? options.write() : { id: 'p', followStatus: 'wait_notice' }
}
b.planApi = {
waitNotice: write,
cancelWaitNotice: write,
cancel: write,
get: async (id: string, mobile: boolean) => {
calls.push(['get', id, mobile])
if (options.failRead) throw Error('offline')
return { ...full, id }
}
}
const f = new Function(
...Object.keys(b),
`let shellReady=true,disposed=false,loadVersion=0,actionVersion=0;${ts.transpile(code, { target: ts.ScriptTarget.ESNext })};return {${names.join(',')}}`
)(...Object.values(b))
return { b, f, calls, full, options }
}
test('status success with scalar PlanRead reloads complete mobile detail for all three actions', async () => {
for (const kind of ['wait', 'cancel-wait', 'void']) {
const h = await statusHarness()
await h.f.changePlanStatus(kind, h.full)
assert.deepEqual(h.b.plan.value.load, h.full.load)
assert.equal(h.b.readDetail.value.transport.id, 'transport')
assert.deepEqual(
h.calls.filter((c) => c[0] === 'get'),
[['get', 'p', true]]
)
assert.equal(h.calls.filter((c) => c[0] === 'write').length, 1)
}
})
test('status write success and failed refresh reports saved and retries only GET', async () => {
const h = await statusHarness({ failRead: true })
await h.f.changePlanStatus('wait', h.full)
assert.match(h.b.errorText.value, /状态已保存.*刷新失败/)
assert.equal(h.b.plan.value, null)
h.options.failRead = false
await h.f.loadDetail()
assert.equal(h.calls.filter((c) => c[0] === 'write').length, 1)
assert.deepEqual(h.b.plan.value.load, h.full.load)
})
test('late status write cannot refresh or replace a new route', async () => {
let release!: (v: any) => void
const waiting = new Promise((resolve) => {
release = resolve
})
const h = await statusHarness({ write: () => waiting })
const action = h.f.changePlanStatus('wait', h.full)
await Promise.resolve()
h.b.recordId.value = 'other'
h.f.invalidateRecord()
await h.f.loadDetail()
release({ id: 'p' })
await action
assert.equal(h.b.plan.value.id, 'other')
assert.deepEqual(
h.calls.filter((c) => c[0] === 'get'),
[['get', 'other', true]]
)
})
test('plan total uses root quantity while stop totals use planQty', async () => {
const { readFileSync } = await import('node:fs')
const { parse } = await import('@vue/compiler-sfc')
const ts = await import('typescript')
const { computed, ref } = await import('vue')
const source = parse(
readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8')
).descriptor.scriptSetup!.content
const ast = ts.createSourceFile('page.ts', source, ts.ScriptTarget.Latest, true)
const names = ['sumProducts', 'loadQty', 'deliveryQty', 'qtyUnitLabel', 'planTotalQtyText']
const code = ast.statements
.filter((node) =>
ts.isFunctionDeclaration(node)
? names.includes(node.name!.text)
: ts.isVariableStatement(node) &&
node.declarationList.declarations.some((d) => names.includes(d.name.getText(ast)))
)
.map((node) => node.getText(ast))
.join('\n')
const state = {
computed,
plan: ref({
products: [
{ quantity: 1, unit: 'pcs' },
{ quantity: 2, unit: 'pcs' }
]
}),
load: ref({ products: [{ planQty: 1 }, { planQty: 2 }] }),
deliveries: ref([{ products: [{ planQty: 1 }] }, { products: [{ planQty: 2 }] }]),
resolveMpProductUnitLabel: () => '个'
}
const values = new Function(
...Object.keys(state),
ts.transpile(code, { target: ts.ScriptTarget.ESNext }) +
';return {planTotalQtyText,loadQty,deliveryQty}'
)(...Object.values(state))
assert.equal(values.planTotalQtyText.value, '3个')
assert.equal(values.loadQty.value, 3)
assert.equal(values.deliveryQty.value, 3)
state.plan.value.products[0].quantity = 0
assert.equal(values.planTotalQtyText.value, '2个')
})