227 lines
10 KiB
TypeScript
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')
|
|
})
|