| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- 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个')
- })
|