Files
shanxia2.0/web-admin/scripts/mobile-pl-plan-arrange.test.ts
T
2026-10-23 23:07:00 +08:00

227 lines
10 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 { ref } from 'vue'
function evaluate(file: string, names: string[], b: Record<string, any>) {
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<string, any>) {
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')
})