import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import vm from 'node:vm' function setup(confirm, saved = new Map(), scope = 'operator:warehouse') { const context = vm.createContext({ reactive: value => value, PortalHeader: {}, ScanTargetInput: {}, playSuccessSound() {}, playErrorSound() {}, confirmReturnReceipt: confirm, uni: { setStorageSync: (key, value) => saved.set(key, structuredClone(value)), getStorageSync: key => saved.get(key), removeStorageSync: key => saved.delete(key) } }) const store = readFileSync(new URL('../src/stores/warehouse-work.js', import.meta.url), 'utf8') vm.runInContext(store.replace(/^import .*$/mg, '').replace(/export /g, ''), context) const page = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8') const script = page.match(/]*>([\s\S]*?)<\/script>/)[1] const component = vm.runInContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), context) vm.runInContext(`restoreWarehouseWork(${JSON.stringify(scope)})`, context) const instance = Object.assign({}, component.methods, { unitType: 'PALLET', palletBoxCount: 1, canConfirm: true, busy: false, current: { returnNo: 'RT-TEST', expectedItems: [{ skuCode: 'SKU-TEST', expectedQuantity: 10, receivedQuantity: 0 }] }, selectedItems: [{ skuCode: 'SKU-TEST' }], photoFileIds: [], trackingNo: ' TRACK-TEST ', startedBy: 'SKU', palletCode: ' PLT-TEST ', palletLength: 10, palletWidth: 10, palletHeight: 10, palletWeight: 1, quantity: () => 2, boxCount: () => 1 }) instance.changeScope = scope => vm.runInContext(`restoreWarehouseWork(${JSON.stringify(scope)})`, context) Object.defineProperty(instance, 'receiptBatch', { get: () => vm.runInContext('warehouseWork.returnReceipt.pendingBatch', context) }) return instance } test('PALLET page submits the required palletCode through the actual confirmation method', async () => { const calls = [] const page = setup(async payload => { calls.push(JSON.parse(JSON.stringify(payload))); return { palletCode: payload.palletCode } }) await page.confirmReceipt() assert.equal(calls.length, 1) assert.equal(calls[0].palletCode, 'PLT-TEST') assert.equal(calls[0].unitCode, undefined) assert.equal(calls[0].returnNo, 'RT-TEST') assert.equal(calls[0].trackingNo, 'TRACK-TEST') assert.equal(calls[0].items[0].receivedQuantity, 2) assert.ok(calls[0].idempotencyKey) assert.equal(page.current, null) }) test('PALLET response failure keeps the draft and same idempotency key for retry', async () => { const calls = [] const page = setup(async payload => { calls.push(JSON.parse(JSON.stringify(payload))); throw new Error('Response lost') }) await page.confirmReceipt() assert.equal(page.current.returnNo, 'RT-TEST') assert.equal(page.busy, false) assert.equal(page.error.message, 'Response lost') await page.confirmReceipt() assert.equal(calls.length, 2) assert.equal(calls[0].idempotencyKey, calls[1].idempotencyKey) }) function boxPage(confirm, saved, scope) { return Object.assign(setup(confirm, saved, scope), { unitType: 'BOX', boxRows: [{ items: { 'SKU-TEST': 1 } }, { items: { 'SKU-TEST': 2 } }] }) } test('BOX partial success resumes only unconfirmed boxes with the original payload and key', async () => { const calls = []; let fail = true const page = boxPage(async payload => { calls.push(structuredClone(payload)) if (payload.items[0].receivedQuantity === 2 && fail) throw new Error('response lost') return { unitType: 'BOX', cartonMark: `BOX-${payload.items[0].receivedQuantity}`, printableHtml: '

label

' } }) await page.confirmReceipt() assert.equal(calls.length, 2) page.boxRows[0].items['SKU-TEST'] = 3 page.boxRows[1].items['SKU-TEST'] = 4 page.trackingNo = 'changed' fail = false await page.confirmReceipt() assert.equal(calls.length, 3) assert.deepEqual(calls[2], calls[1]) assert.equal(page.result.labels.length, 2) assert.equal(page.receiptBatch, null) }) test('BOX progress survives a new page instance even when the order no longer allows a fresh receipt', async () => { const saved = new Map(); const calls = []; let fail = true const confirm = async payload => { calls.push(structuredClone(payload)) if (payload.items[0].receivedQuantity === 2 && fail) throw new Error('response lost') return { unitType: 'BOX', cartonMark: `BOX-${payload.items[0].receivedQuantity}`, printableHtml: '

label

' } } await boxPage(confirm, saved).confirmReceipt() const restored = boxPage(confirm, saved) restored.current = null; restored.canConfirm = false fail = false await restored.confirmReceipt() assert.equal(calls.length, 3) assert.deepEqual(calls[2], calls[1]) assert.equal(restored.result.labels.length, 2) }) test('pending BOX receipt cannot be discarded by switching order and does not leak to another operator', async () => { const saved = new Map() const page = boxPage(async () => { throw new Error('response lost') }, saved) await page.confirmReceipt() page.clearCurrent() assert.ok(page.receiptBatch) assert.equal(page.current.returnNo, 'RT-TEST') const other = boxPage(async () => { throw new Error('must not submit') }, saved, 'other:warehouse') assert.equal(other.receiptBatch, null) }) test('BOX request is not sent when the immutable batch cannot be persisted', async () => { class FailingStorage extends Map { set(key, value) { if (value.returnReceipt?.pendingBatch) throw new Error('storage unavailable'); return super.set(key, value) } } let calls = 0 const page = boxPage(async () => { calls++; return {} }, new FailingStorage()) await page.confirmReceipt() assert.equal(calls, 0) assert.equal(page.error.message, 'storage unavailable') assert.equal(page.busy, false) }) test('BOX lost progress persistence replays only with the same durable key after reload', async () => { class OnceFailingStorage extends Map { fail = true set(key, value) { if (this.fail && value.returnReceipt?.pendingBatch?.entries[0].result) { this.fail = false; throw new Error('storage unavailable') } return super.set(key, value) } } const saved = new OnceFailingStorage(); const calls = [] const confirm = async payload => { calls.push(structuredClone(payload)); return { unitType: 'BOX', cartonMark: 'BOX', printableHtml: '

label

' } } const first = boxPage(confirm, saved) await first.confirmReceipt() assert.equal(calls.length, 1) assert.equal(first.error.message, 'storage unavailable') const restored = boxPage(confirm, saved) await restored.confirmReceipt() assert.equal(calls.length, 3) assert.deepEqual(calls[0], calls[1]) assert.equal(restored.result.labels.length, 2) }) test('BOX response arriving after account change cannot repopulate another account draft', async () => { const saved = new Map(); let calls = 0 const page = boxPage(async () => { calls++ // Simulate the real shared store changing identity while the request is in flight. page.changeScope('other:warehouse') return { unitType: 'BOX', cartonMark: 'BOX' } }, saved) await page.confirmReceipt() assert.equal(calls, 1) assert.equal(page.receiptBatch, null) assert.match(page.error.message, /账号或仓库已改变/) })