2026-10-02 11:12:56 +08:00
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import * as store from '../src/stores/receiving.js'
2026-10-28 17:17:54 +08:00
import { normalizeReceivingSession , toReceivingError } from '../src/api/receiving-adapter.js'
2026-10-02 11:12:56 +08:00
const source = readFileSync ( new URL ( '../src/pages/operation/operation.vue' , import . meta . url ), 'utf8' ). match ( /<script>([\s\S]*?)<\/script>/ )[ 1 ]
const defer = () => { let resolve , reject ; const promise = new Promise (( a , b ) => { resolve = a ; reject = b }); return { promise , resolve , reject } }
const sessionValue = ( patch = {}) => normalizeReceivingSession ({ containerNo : 'CAB-A' , preparedAt : '2026-10-02T10:00:00' ,
overseasWarehouseId : '900' , status : 'SIGNED' , inbounds : [{ inboundCode : 'IN-A' , customerId : '100' }],
photos : [ 'LICENSE_PLATE' , 'CONTAINER_SEAL' , 'DOOR_OPEN' ]. map (( role , i ) => ({ role , fileId : String ( i + 1 ), downloadPath : `/signed/ ${ i + 1 } ` })),
domesticLoadingPhotos : Array . from ({ length : 5 }, ( _ , i ) => ({ evidenceType : `PHOTO_ ${ i } ` , downloadPath : `/domestic/ ${ i } ` })), ... patch })
function page ( overrides = {}) {
const deps = { ... store , session : { tenantId : '100' , currentUser : { id : 'operator' , warehouseId : '900' } },
PortalHeader : {}, ReceivingSessionSummary : {}, ScanTargetInput : {}, requireSession : () => true ,
startScanBroadcast : () => () => {}, playErrorSound () {}, playSuccessSound () {},
receivingPhase : store . receivingPhase , toReceivingError : error => error , ... overrides }
const definition = new Function (... Object . keys ( deps ), source . replace ( /^\s*import .*$/gm , '' ). replace ( 'export default' , 'return' ))(... Object . values ( deps ))
const vm = { ... definition . data (), _pageActive : true , _scanEpoch : 0 }
for ( const [ name , method ] of Object . entries ( definition . methods )) vm [ name ] = method . bind ( vm )
for ( const [ name , descriptor ] of Object . entries ( definition . computed )) Object . defineProperty ( vm , name ,
typeof descriptor === 'function' ? { get : () => descriptor . call ( vm ) } : { get : () => descriptor . get . call ( vm ), set : value => descriptor . set . call ( vm , value ) })
vm . moduleKey = 'inbound' ; vm . validateReceivingWarehouse = value => value ; vm . showToast = () => {}
return { vm , definition , deps }
}
test ( 'same current readback retains three signed previews; collapsed unversioned domestic images issue no downloads' , async () => {
store . resetReceivingSession (); const calls = []
const download = async path => { calls . push ( path ); return `local: ${ path } ` }
const { vm } = page ({ downloadCurrentUnloadPhoto : download , downloadCurrentDomesticLoadingPhoto : download })
await vm . applyCurrentReceiving ( sessionValue ({ status : 'WAIT_SIGN' })); assert . equal ( calls . length , 8 )
await vm . applyCurrentReceiving ( sessionValue ()); await vm . applyCurrentReceiving ( sessionValue ()); assert . equal ( calls . length , 8 )
await vm . toggleDomesticPhotos (); assert . equal ( calls . length , 13 )
await vm . restoreDomesticPhotoPreviews (); assert . equal ( calls . length , 13 )
// Domestic DTO has no file identity/version: a new response must not reuse an old image.
await vm . applyCurrentReceiving ( sessionValue ()); assert . equal ( calls . length , 18 )
})
2026-10-28 17:17:54 +08:00
test ( 'editing and rejecting a pallet cannot submit using the previous successful validation' , async t => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
store . receiving . resolvedCarton = { inboundCode : 'IN-A' , cartonMark : 'IN-A#22-1' , destinationType : 'FBA' }
store . receiving . resolvedPallet = { palletCode : 'PALLET-OLD' , compatible : true , version : 1 }
let writes = 0
const { vm } = page ({ toReceivingError ,
resolveCurrentPallet : async () => { throw Object . assign ( new Error ( 'Pallet code is not part of the current FBA destination group' ), { code : 'A0404' , requestId : 'scan-request' , traceId : 'scan-trace' }) },
receiveCurrentBulk : async () => { writes ++ },
})
t . after (() => vm . suspendReceiving ())
vm . palletInput = 'PALLET-OLD' ; vm . packageCount = 3
assert . equal ( vm . canSubmitReceipt , true )
vm . palletInput = 'PALLET-WRONG'
assert . equal ( vm . canSubmitReceipt , false )
const toasts = []; vm . showToast = ( message , type ) => toasts . push ({ message , type })
await vm . resolvePallet ( vm . palletInput )
assert . equal ( vm . resolvedPallet , null )
assert . equal ( vm . packageCount , '' )
assert . equal ( vm . activeTarget , 'pallet' )
assert . equal ( vm . error . message , '托盘不属于当前目的仓,请重扫' )
assert . equal ( vm . error . requestId , 'scan-request' )
assert . equal ( vm . error . traceId , 'scan-trace' )
assert . deepEqual ( toasts , [{ message : '托盘不属于当前目的仓,请重扫' , type : 'error' }])
await vm . submitReceipt ()
assert . equal ( writes , 0 )
})
test ( 'an incompatible pallet never reports success and a corrected scan can be validated normally' , async t => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
store . receiving . resolvedCarton = { inboundCode : 'IN-A' , cartonMark : 'IN-A#22-1' , destinationType : 'FBA' }
const calls = []; let successes = 0
const { vm } = page ({ toReceivingError , playSuccessSound : () => successes ++ ,
resolveCurrentPallet : async ( palletCode , inboundCode ) => {
calls . push ({ palletCode , inboundCode })
return { palletCode , compatible : palletCode === 'PALLET-VALID' , version : 2 , currentReceivedPackages : 3 }
},
})
t . after (() => vm . suspendReceiving ())
const toasts = []; vm . showToast = ( message , type ) => toasts . push ({ message , type })
await vm . resolvePallet ( 'PALLET-WRONG' )
assert . equal ( vm . resolvedPallet , null )
assert . equal ( vm . canSubmitReceipt , false )
assert . equal ( successes , 0 )
assert . deepEqual ( toasts , [{ message : '托盘不匹配,请重扫' , type : 'error' }])
await vm . resolvePallet ( 'PALLET-VALID' )
assert . deepEqual ( calls , [{ palletCode : 'PALLET-WRONG' , inboundCode : 'IN-A' }, { palletCode : 'PALLET-VALID' , inboundCode : 'IN-A' }])
assert . equal ( vm . resolvedPallet . palletCode , 'PALLET-VALID' )
assert . equal ( vm . error , null )
assert . equal ( vm . packageCount , 3 )
assert . equal ( vm . canSubmitReceipt , true )
assert . equal ( vm . activeTarget , 'quantity' )
assert . equal ( successes , 1 )
})
2026-10-28 17:32:46 +08:00
test ( 'late pallet validation cannot replace edited input and only the corrected pallet can be received' , async t => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
store . receiving . resolvedCarton = { inboundCode : 'IN-A' , cartonMark : 'IN-A#22-1' , destinationType : 'FBA' }
const pending = defer (), receipts = [], calls = [], toasts = []; let successes = 0
const { vm } = page ({ toReceivingError , playSuccessSound : () => successes ++ ,
resolveCurrentPallet : ( palletCode , inboundCode ) => {
calls . push ({ palletCode , inboundCode })
return palletCode === 'PALLET-A' ? pending . promise : Promise . resolve ({ palletCode , compatible : true , version : 2 , currentReceivedPackages : 3 })
},
receiveCurrentBulk : async payload => { receipts . push ( payload ); return {} },
getCurrentReceiving : async () => sessionValue (),
})
t . after (() => vm . suspendReceiving ())
vm . showToast = ( message , type ) => toasts . push ({ message , type }); vm . applyCurrentReceiving = async () => {}
const validation = vm . resolvePallet ( 'PALLET-A' )
vm . palletInput = 'PALLET-B'
pending . resolve ({ palletCode : 'PALLET-A' , compatible : true , version : 1 , currentReceivedPackages : 3 })
await validation
assert . equal ( vm . palletInput , 'PALLET-B' )
assert . equal ( vm . resolvedPallet , null )
assert . equal ( vm . packageCount , '' )
assert . equal ( vm . canSubmitReceipt , false )
assert . equal ( vm . busy , false )
assert . equal ( successes , 0 )
assert . deepEqual ( toasts , [])
await vm . submitReceipt (); assert . equal ( receipts . length , 0 )
await vm . resolvePallet ( 'PALLET-B' )
assert . equal ( vm . canSubmitReceipt , true )
assert . equal ( successes , 1 )
await vm . submitReceipt ()
assert . deepEqual ( calls , [{ palletCode : 'PALLET-A' , inboundCode : 'IN-A' }, { palletCode : 'PALLET-B' , inboundCode : 'IN-A' }])
assert . deepEqual ( receipts . map (({ palletCode }) => palletCode ), [ 'PALLET-B' ])
})
test ( 'obsolete pallet failures cannot replace current input or feedback' , async t => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
store . receiving . resolvedCarton = { inboundCode : 'IN-A' , cartonMark : 'IN-A#22-1' , destinationType : 'FBA' }
const pending = defer (), toasts = []; let failures = 0
const { vm } = page ({ toReceivingError , resolveCurrentPallet : () => pending . promise , playErrorSound : () => failures ++ })
t . after (() => vm . suspendReceiving ()); vm . showToast = ( message , type ) => toasts . push ({ message , type })
const validation = vm . resolvePallet ( 'PALLET-A' )
vm . palletInput = 'PALLET-B'
pending . reject ( Object . assign ( new Error ( 'Pallet code is not part of the current FBA destination group' ), { code : 'A0404' }))
await validation
assert . equal ( vm . palletInput , 'PALLET-B' ); assert . equal ( vm . error , null )
assert . equal ( vm . resolvedPallet , null ); assert . equal ( vm . canSubmitReceipt , false )
assert . equal ( vm . busy , false ); assert . equal ( failures , 0 ); assert . deepEqual ( toasts , [])
})
test ( 'late pallet validation cannot cross a receiving context or page lifetime' , async t => {
for ( const [ name , change ] of [
[ 'carton' , () => { store . receiving . resolvedCarton = { inboundCode : 'IN-B' , cartonMark : 'IN-B#1' , destinationType : 'FBA' } }],
[ 'container' , () => store . setCurrentReceiving ( sessionValue ({ containerNo : 'CAB-B' }))],
[ 'operator' , ({ deps }) => { deps . session . currentUser = { id : 'another-operator' , warehouseId : '900' } }],
[ 'warehouse' , ({ deps }) => { deps . session . currentUser . warehouseId = '901' }],
[ 'hide and return' , ({ vm , definition }) => { definition . onHide . call ( vm ); definition . onShow . call ( vm ) }],
]) await t . test ( name , async t => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
store . receiving . resolvedCarton = { inboundCode : 'IN-A' , cartonMark : 'IN-A#22-1' , destinationType : 'FBA' }
const pending = defer (), toasts = []; let successes = 0
const context = page ({ toReceivingError , resolveCurrentPallet : () => pending . promise , playSuccessSound : () => successes ++ })
const { vm } = context
t . after (() => vm . suspendReceiving ()); vm . bootstrap = async () => {}; vm . showToast = ( message , type ) => toasts . push ({ message , type })
const validation = vm . resolvePallet ( 'PALLET-A' )
change ( context )
pending . resolve ({ palletCode : 'PALLET-A' , compatible : true , version : 1 , currentReceivedPackages : 3 })
await validation
assert . equal ( vm . resolvedPallet , null ); assert . equal ( vm . packageCount , '' )
assert . equal ( vm . canSubmitReceipt , false ); assert . equal ( vm . busy , false )
assert . equal ( successes , 0 ); assert . deepEqual ( toasts , [])
})
})
2026-10-02 11:12:56 +08:00
test ( 'signed photo changes, account/customer/cabinet changes and image failure cannot inherit stale previews' , async () => {
store . resetReceivingSession (); let calls = 0
const { vm , deps } = page ({ downloadCurrentUnloadPhoto : async () => `local: ${ ++ calls } ` , downloadCurrentDomesticLoadingPhoto : async () => '' })
await vm . applyCurrentReceiving ( sessionValue ()); assert . equal ( calls , 3 )
const changed = sessionValue (); changed . photos [ 0 ]. fileId = 'NEW' ; await vm . applyCurrentReceiving ( changed ); assert . equal ( calls , 4 )
deps . session . tenantId = '200' ; await vm . applyCurrentReceiving ( changed ); assert . equal ( calls , 7 )
await vm . applyCurrentReceiving ( sessionValue ({ containerNo : 'CAB-B' })); assert . equal ( calls , 10 )
await vm . applyCurrentReceiving ( sessionValue ({ containerNo : 'CAB-B' , inbounds : [{ inboundCode : 'IN-OTHER' , customerId : '201' }] })); assert . equal ( calls , 13 )
vm . invalidatePhotoPreview ( store . receiving . photoSlots [ 0 ]); await vm . restorePhotoPreviews (); assert . equal ( calls , 14 )
})
test ( 'overlapping current refresh joins inflight photo download and failed fetch remains retryable' , async () => {
store . resetReceivingSession (); const pending = defer (); let calls = 0
const { vm } = page ({ downloadCurrentUnloadPhoto : async path => { calls ++ ; await pending . promise ; return `local: ${ path } ` } })
const first = vm . applyCurrentReceiving ( sessionValue ()); await Promise . resolve ()
const second = vm . applyCurrentReceiving ( sessionValue ()); await Promise . resolve (); assert . equal ( calls , 3 )
pending . resolve (); await Promise . all ([ first , second ]); assert . equal ( calls , 3 )
const photo = { downloadPath : '/retry' }; let attempts = 0
const download = async () => { if ( ++ attempts === 1 ) throw new Error ( 'offline' ); return 'local:retry' }
await assert . rejects ( store . restoreReceivingPhoto ( photo , download ), /offline/ )
assert . equal ( photo . status , 'error' ); assert . equal ( await store . restoreReceivingPhoto ( photo , download ), 'local:retry' ); assert . equal ( attempts , 2 )
})
test ( 'hide and unload discard queued scans and cancel drain scheduled by an in-flight request; return refreshes facts' , async () => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
const pending = defer (); const { vm , definition } = page (); let submissions = 0 , refreshes = 0
vm . dispatchHardwareScan = () => { submissions ++ }; vm . bootstrap = async () => { refreshes ++ }
vm . scanQueue = [{ value : 'SECOND' , target : 'carton' }]
const running = vm . run ( 'already sent' , async () => { submissions ++ ; await pending . promise })
definition . onHide . call ( vm ); pending . resolve (); await running
await new Promise ( resolve => setTimeout ( resolve , 5 )); assert . equal ( submissions , 1 ); assert . equal ( vm . scanQueue . length , 0 )
definition . onShow . call ( vm ); assert . equal ( refreshes , 1 ); assert . equal ( submissions , 1 )
vm . scanQueue . push ({ value : 'THIRD' }); vm . scheduleScanDrain (); definition . onUnload . call ( vm )
await new Promise ( resolve => setTimeout ( resolve , 5 )); assert . equal ( submissions , 1 )
})
test ( 'hide during cargo resolution prevents the subsequent automatic dropship write, including quick return' , async () => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
const pending = defer (); let writes = 0
const { vm , definition } = page ({ resolveCurrentCarton : () => pending . promise , receiveCurrentWholePallet : async () => { writes ++ } })
vm . bootstrap = async () => {}
const running = vm . resolveCarton ( 'DP-1' ); definition . onHide . call ( vm ); definition . onShow . call ( vm )
pending . resolve ({ operationMode : 'DROPSHIP_PALLET' , sourcePalletNo : 'DP-1' }); await running
assert . equal ( writes , 0 ); assert . equal ( vm . scanQueue . length , 0 )
})
test ( 'already sent receipt retains successful server fact after hide without draining another scan' , async () => {
store . resetReceivingSession (); store . setCurrentReceiving ( sessionValue ())
const pending = defer (); let writes = 0 , reads = 0
const { vm , definition } = page ({ resolveCurrentCarton : async () => ({ operationMode : 'DROPSHIP_PALLET' , sourcePalletNo : 'DP-1' }),
receiveCurrentWholePallet : async () => { writes ++ ; return pending . promise },
getCurrentReceiving : async () => { reads ++ ; return sessionValue ({ receivedPackages : 1 }) } })
const running = vm . resolveCarton ( 'DP-1' ); await Promise . resolve (); assert . equal ( writes , 1 )
vm . scanQueue . push ({ value : 'DP-2' , target : 'carton' }); definition . onHide . call ( vm )
pending . resolve ({ receivedPackages : 1 }); await running
assert . equal ( writes , 1 ); assert . equal ( reads , 1 ); assert . equal ( store . receiving . current . receivedPackages , 1 )
assert . equal ( store . receiving . lastReceipt . receivedPackages , 1 ); assert . equal ( vm . scanQueue . length , 0 )
})
test ( 'late old photo download never overwrites replacement or cross-account preview' , async () => {
store . resetReceivingSession (); const first = defer (), calls = []
const { vm , deps } = page ({ downloadCurrentUnloadPhoto : async path => { calls . push ( path ); if ( calls . length <= 3 ) await first . promise ; return `local: ${ path } ` } })
const old = vm . applyCurrentReceiving ( sessionValue ()); await Promise . resolve ()
deps . session . tenantId = 'new-customer'
const changed = sessionValue (); changed . photos [ 0 ] = { ... changed . photos [ 0 ], fileId : 'new' , downloadPath : '/signed/new' }
await vm . applyCurrentReceiving ( changed ); assert . equal ( calls . length , 6 )
first . resolve (); await old
assert . equal ( store . receiving . photoSlots [ 0 ]. fileId , 'new' ); assert . equal ( store . receiving . photoSlots [ 0 ]. localPath , 'local:/signed/new' )
})