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 { ref } from 'vue' function evaluate(file: string, names: string[], b: Record) { const ast = ts.createSourceFile( 'x.ts', parse(readFileSync(new URL(file, import.meta.url), 'utf8')).descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const text = names .map((name) => { const n = ast.statements.find((n) => ts.isFunctionDeclaration(n) ? n.name?.text === name : ts.isVariableStatement(n) && n.declarationList.declarations.some((d) => d.name.getText(ast) === name) ) assert.ok(n, name) return n.getText(ast) }) .join('\n') return new Function( ...Object.keys(b), ts.transpile(text, { target: ts.ScriptTarget.ESNext }) + ';return {' + names.join(',') + '}' )(...Object.values(b)) } const file = '../src/components/business/pl-plan-form-dialog/index.vue' function harness(overrides: any = {}) { const writes: string[] = [], events: any[] = [] const yes = () => true, noop = () => {}, b: any = { ref, isPageVariant: ref(true), pageReady: ref(true), mobileMetadata: { ready: yes }, mirrorPlanProductsFromLoad: noop, buildMobileProductLinks: noop, loadForm: ref({ products: [] }), deliveryForms: ref([]), formData: { products: [], vehicleId: 'v1' }, pageLoadGeneration: 1, isPageSubmitCurrent: yes, isPageLoadCurrent: yes, submitLoading: ref(false), mobileSavedPlanId: ref(''), mobileCreateUncertain: ref(false), props: {}, isEdit: ref(false), basicFormRef: ref({ validate: async () => true }), validateBusinessRules: yes, prefetchStopContractProducts: async () => {}, validateSingleContractPerStop: yes, validateContractPackageSpecs: yes, validateStopContractsRequired: yes, getArrangePrerequisiteGaps: () => [], validateStopProductsForArrange: yes, buildPlanCommonPayload: () => ({}), planApi: { createFull: async () => { writes.push('create') return { id: 'p1' } }, update: async () => { writes.push('update') } }, pickBxData: (x: any) => x, showPlanSaveWarnings: noop, ElMessage: { warning: noop, error: noop, success: noop }, innerVisible: ref(true), emit: (...a: any[]) => events.push(a), performArrangeAfterSave: async () => { writes.push('arrange') throw Error('get failed') }, ...overrides } return { ...evaluate(file, ['handleSubmit'], b), b, writes, events } } test('create success then GET failure retains identity and emits saved result', async () => { const h = harness() await h.handleSubmit('saveAndArrange') assert.equal(h.b.mobileSavedPlanId.value, 'p1') assert.deepEqual(h.events.at(-1), ['success', { planId: 'p1', arranged: false }]) }) test('double click creates once', async () => { const h = harness() await Promise.all([h.handleSubmit('saveAndArrange'), h.handleSubmit('saveAndArrange')]) assert.equal(h.writes.filter((x) => x === 'create').length, 1) }) for (const reason of ['certificate', 'overflow', 'cancel', 'arrange500', 'get']) test('actual arrange handler preserves saved ID after ' + reason, async () => { const h = harness() Object.assign(h.b, { loadedPlanSnapshot: ref(null), selectedDriverId: ref(''), selectedEscortId: ref(''), freightCalc: {}, getUnitLabel: (x: any) => x, enrichPlanProductsHazardFromCrm: async (x: any) => x, buildMissingHazardCertReadyMessage: () => (reason === 'certificate' ? '缺少证件' : ''), createArrangePresenter: () => async () => {}, ElMessageBox: { alert: async () => {} }, planHasAnyDibangSelected: () => false, loadArrangeHistoricalFlags: async () => ({}), confirmBeforePlanArrange: async () => { if (reason === 'cancel') throw 'cancel' }, buildArrangePayloadFromPlanForm: () => ({}), submitPlanArrange: async () => { throw { response: { data: { detail: { code: reason === 'overflow' ? 'CONTRACT_QTY_OVERFLOW' : 'SERVER_ERROR' } } } } } }) h.b.planApi.get = async () => { if (reason === 'get') throw Error('GET failed') return { id: 'p1', products: [], transportStatus: 'arranged' } } h.b.performArrangeAfterSave = evaluate( file, ['performArrangeAfterSave'], h.b ).performArrangeAfterSave const handler = evaluate(file, ['handleSubmit'], h.b).handleSubmit await handler('saveAndArrange') assert.equal(h.b.mobileSavedPlanId.value, 'p1') await handler('saveAndArrange') assert.equal(h.writes.filter((x) => x === 'create').length, 1) assert.deepEqual(h.events.at(-1), ['success', { planId: 'p1', arranged: false }]) }) test('uncertain create locks retry', async () => { let count = 0 const h = harness({ planApi: { createFull: async () => { count++ throw Error('network') } } }) await h.handleSubmit('saveAndArrange') await h.handleSubmit('saveAndArrange') assert.equal(count, 1) assert.equal(h.b.mobileCreateUncertain.value, true) }) test('adjustment only updates', async () => { const h = harness({ props: { planId: 'existing' }, isEdit: ref(true) }) await h.handleSubmit('rearrange') assert.deepEqual(h.writes, ['update', 'arrange']) }) test('quick create selects returned vehicle without resetting stop choices or unsaved fields', async () => { const formData = { vehicleId: 'old', remark: 'unsaved' }, calls: any[] = [] const b = { isPageVariant: ref(true), formData, pageLoadGeneration: 1, isPageSubmitCurrent: () => true, ensureSelectedVehicleOption: async () => {}, handleVehicleChange: async (...args: any[]) => calls.push(args), handleVehicleLibrarySelect: async () => {}, ElMessage: { warning: () => {} } } const done = evaluate(file, ['handlePasteRecognizeDone'], b).handlePasteRecognizeDone await done({ vehicleId: 'new' }) assert.equal(formData.vehicleId, 'new') assert.equal(formData.remark, 'unsaved') assert.equal(calls[0][1].syncStopDibang, false) }) test('explicit validation rejection unlocks correction retry', async () => { let count = 0 const h = harness({ planApi: { createFull: async () => { count++; throw { response: { status: 422 } } } } }) await h.handleSubmit('saveAndArrange').catch(() => {}) assert.equal(h.b.mobileCreateUncertain.value, false) await h.handleSubmit('saveAndArrange').catch(() => {}) assert.equal(count, 2) }) test('new plan session clears saved identity and uncertain result', async () => { const b = { isPageVariant: ref(true), mobileSavedPlanId: ref('old'), mobileCreateUncertain: ref(true), pageLoadGeneration: 1, pageReady: ref(true), arrangeSheetsRef: ref(null), props: { visible: false }, handleOpened: async () => {} } const run = evaluate(file, ['handleMobilePlanSessionChange'], b).handleMobilePlanSessionChange await run() assert.equal(b.mobileSavedPlanId.value, '') assert.equal(b.mobileCreateUncertain.value, false) assert.equal(b.pageReady.value, false) }) function arrangeFunctions(bindings: Record) { const source = readFileSync(new URL('../src/composables/usePlPlanArrangeSubmit.ts', import.meta.url), 'utf8') const ast = ts.createSourceFile('arrange.ts', source, ts.ScriptTarget.Latest, true) const text = ast.statements.filter(n => ts.isFunctionDeclaration(n) && ['confirmBeforePlanArrange', 'submitPlanArrange'].includes(n.name!.text)).map(n => n.getText(ast).replace(/^export /, '')).join('\n') return new Function(...Object.keys(bindings), ts.transpile(text, { target: ts.ScriptTarget.ESNext }) + ';return {confirmBeforePlanArrange,submitPlanArrange}')(...Object.values(bindings)) } test('arrange presenter replaces HTML dialogs while PC keeps original calls and payload', async () => { const calls:any[] = [], payload = { vehicleId:'v', driverId:undefined, escortId:undefined } const b = { planRequiresSiteLink:()=>false, ElMessageBox:{confirm:async(...args:any[])=>calls.push(['pc',...args])}, transportApi:{arrange:async(id:string,body:any)=>{calls.push(['api',id,body]);return {contractQtyReminder:{items:[{remainingQty:2}]}}}}, pickBxData:(x:any)=>x, showContractQtyReminderIfAny:async()=>calls.push(['pc-reminder']), showContractQtyViolationIfMatch:async()=>calls.push(['pc-violation']), extractContractQtyViolationDetail:(e:any)=>e.response?.data?.detail?.code==='CONTRACT_QTY_OVERFLOW'?e.response.data.detail:null } const f=arrangeFunctions(b), presenter=async(request:any)=>{calls.push(['mobile',request])} await f.confirmBeforePlanArrange({isRearrange:true,plan:{},presenter}) assert.equal(calls[0][0],'mobile');assert.equal(calls[0][1].confirmText,'继续调整') await f.submitPlanArrange('p',payload,presenter) assert.equal(calls[1][2],payload);assert.equal(calls[2][1].items[0].remainingQty,2) await f.confirmBeforePlanArrange({isRearrange:true,plan:{}}) assert.deepEqual(calls[3],['pc','司机链接将更新,旧链接将失效。是否继续?','调整运输确认',{type:'warning',confirmButtonText:'继续调整',cancelButtonText:'取消'}]) const failure={response:{data:{detail:{code:'CONTRACT_QTY_OVERFLOW',message:'超量',items:[{remainingQty:-1}]}}}} b.transportApi.arrange=async()=>{throw failure} await assert.rejects(f.submitPlanArrange('p',payload,presenter),e=>e===failure) assert.equal(calls.at(-1)[1].items[0].remainingQty,-1) }) test('generation-bound presenter ignores old arrange response and session change settles old sheet',async()=>{ let generation=1;const presented:any[]=[],closed:any[]=[] const b={isPageSubmitCurrent:(value:number)=>value===generation,arrangeSheetsRef:ref({present:async(value:any)=>presented.push(value),close:()=>closed.push(true)}),isPageVariant:ref(true),mobileSavedPlanId:ref('old'),mobileCreateUncertain:ref(true),pageLoadGeneration:1,pageReady:ref(true),props:{visible:false},handleOpened:async()=>{}} const f=evaluate(file,['createArrangePresenter','handleMobilePlanSessionChange'],b) const old=f.createArrangePresenter(1);await f.handleMobilePlanSessionChange();generation=2 await old({title:'old response'});assert.equal(presented.length,0);assert.equal(closed.length,1) await f.createArrangePresenter(2)({title:'new response'});assert.equal(presented[0].title,'new response') })