Files

109 lines
8.4 KiB
JavaScript
Raw Permalink Normal View History

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import * as store from '../src/stores/receiving.js'
import { normalizeReceivingSession } from '../src/api/receiving-adapter.js'
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)
})
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')
})