fix(pda): align return workflows with current test backend
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { findPackingOrders, getReturnCartonLabel, listReturnPackingPlans, mergeReturnCartons, normalizePackingPreview } from '../src/api/return-packing.js'
|
||||
import { findPackingOrders, getReturnCartonLabel, mergeReturnCartons, normalizePackingPreview } from '../src/api/return-packing.js'
|
||||
import { session } from '../src/stores/session.js'
|
||||
|
||||
test('packing preview keeps customer scope internal and exposes stable order references', () => {
|
||||
@@ -40,11 +40,3 @@ test('return packing lookup still rejects real service errors', async () => {
|
||||
globalThis.uni = { request(options) { options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) } }
|
||||
await assert.rejects(() => findPackingOrders('RT-1'), (error) => error.code === 'B0500')
|
||||
})
|
||||
|
||||
test('plan list distinguishes an empty success from a missing API and preserves diagnostics', async () => {
|
||||
session.apiBase = '/api'; session.token = 'packing-token'
|
||||
globalThis.uni = { request(options) { options.success({ statusCode: 200, data: { code: '200', data: [] }, header: {} }) } }
|
||||
assert.deepEqual(await listReturnPackingPlans(), [])
|
||||
globalThis.uni = { request(options) { options.success({ statusCode: 404, data: { code: 'A0404', message: '计划接口不存在', requestId: 'missing-plan-api' }, header: {} }) } }
|
||||
await assert.rejects(() => listReturnPackingPlans(), error => error.code === 'A0404' && error.requestId === 'missing-plan-api')
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
function setup(merge) {
|
||||
const saved = new Map()
|
||||
const context = vm.createContext({ reactive: value => value, PortalHeader: {}, ScanTargetInput: {},
|
||||
playSuccessSound() {}, playErrorSound() {}, mergeReturnPackingPlan: merge,
|
||||
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 source = readFileSync(new URL('../src/pages/return-packing/return-packing.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1]
|
||||
const component = vm.runInContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), context)
|
||||
vm.runInContext("restoreWarehouseWork('operator:warehouse')", context)
|
||||
const page = Object.assign({}, component.methods, { canCompletePlan: true, busy: false,
|
||||
activePlan: { planNo: 'PLAN-1' }, planBoxes: [{ items: [{ skuCode: ' SKU-1 ', quantity: '2' }], length: 10, width: 20, height: 30, weight: 1 }],
|
||||
async loadPlans() {} })
|
||||
return { page, restore: scope => vm.runInContext(`restoreWarehouseWork(${JSON.stringify(scope)})`, context),
|
||||
keys: () => vm.runInContext('warehouseWork.returnPackingPlan.pendingKeys', context) }
|
||||
}
|
||||
|
||||
test('plan completion reaches API and preserves the key across lost responses and draft restore', async () => {
|
||||
const calls = []; let fail = true
|
||||
const fixture = setup(async (plan, payload) => { calls.push({ plan, payload }); if (fail) throw new Error('response lost'); return { labels: [] } })
|
||||
await fixture.page.completePlan()
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(fixture.page.error.message, 'response lost')
|
||||
assert.equal(fixture.page.busy, false)
|
||||
fixture.restore('operator:warehouse')
|
||||
fail = false
|
||||
await fixture.page.completePlan()
|
||||
assert.equal(calls.length, 2)
|
||||
assert.equal(calls[0].payload.idempotencyKey, calls[1].payload.idempotencyKey)
|
||||
assert.equal(calls[1].payload.boxes[0].items[0].skuCode, 'SKU-1')
|
||||
assert.equal(fixture.page.activePlan, null)
|
||||
assert.equal(Object.keys(fixture.keys()).length, 0)
|
||||
})
|
||||
|
||||
test('plan completion keys are not reused by another operator or warehouse', async () => {
|
||||
const keys = []
|
||||
const fixture = setup(async (_plan, payload) => { keys.push(payload.idempotencyKey); throw new Error('response lost') })
|
||||
await fixture.page.completePlan()
|
||||
fixture.restore('other-operator:warehouse')
|
||||
await fixture.page.completePlan()
|
||||
assert.equal(keys.length, 2)
|
||||
assert.notEqual(keys[0], keys[1])
|
||||
})
|
||||
@@ -20,8 +20,6 @@ function setup(confirm, saved = new Map(), scope = 'operator:warehouse') {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -32,6 +30,8 @@ test('PALLET page submits the required palletCode through the actual confirmatio
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].palletCode, 'PLT-TEST')
|
||||
assert.equal(calls[0].unitCode, undefined)
|
||||
assert.deepEqual(Object.keys(calls[0]).sort(), ['idempotencyKey', 'items', 'palletCode', 'returnNo', 'startedBy', 'trackingNo'].sort())
|
||||
assert.deepEqual(Object.keys(calls[0].items[0]).sort(), ['receivedQuantity', 'skuCode'].sort())
|
||||
assert.equal(calls[0].returnNo, 'RT-TEST')
|
||||
assert.equal(calls[0].trackingNo, 'TRACK-TEST')
|
||||
assert.equal(calls[0].items[0].receivedQuantity, 2)
|
||||
@@ -50,103 +50,3 @@ test('PALLET response failure keeps the draft and same idempotency key for retry
|
||||
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: '<p>label</p>' }
|
||||
})
|
||||
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: '<p>label</p>' }
|
||||
}
|
||||
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: '<p>label</p>' } }
|
||||
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, /账号或仓库已改变/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user