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(null), recordId = ref('older'), readDetail = ref(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 } = {}) { 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个') })