188 lines
9.6 KiB
JavaScript
188 lines
9.6 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { runInNewContext } from 'node:vm'
|
|
|
|
const source = readFileSync(new URL('../src/pages/return-counting/return-counting.vue', import.meta.url), 'utf8')
|
|
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^import .+$/gm, '').replace('export default', 'page =')
|
|
const scope = { PortalHeader: {}, ScanTargetInput: {} }
|
|
runInNewContext(script, scope)
|
|
|
|
function state(a = '2', b = '2', c = '2') {
|
|
const value = {
|
|
scanCode: '', selectedMark: '',
|
|
task: { status: 'COUNTING', expectedItems: [{ skuCode: 'A', quantity: '4' }, { skuCode: 'B', quantity: '2' }], boxes: [
|
|
{ cartonMark: 'RC#2-1', items: [{ skuCode: 'A', quantity: a }] },
|
|
{ cartonMark: 'RC#2-2', items: [{ skuCode: 'A', quantity: b }] },
|
|
{ cartonMark: 'RC#1-1', items: [{ skuCode: 'B', quantity: c }] }
|
|
] }
|
|
}
|
|
value.skuCodes = scope.page.computed.skuCodes.call(value)
|
|
value.totals = scope.page.computed.totals.call(value)
|
|
return value
|
|
}
|
|
|
|
test('list entry can confirm without scanning when string wire quantities reconcile', () => {
|
|
const value = state()
|
|
assert.equal(value.totals[0].quantity, 4)
|
|
assert.equal(value.totals[1].quantity, 2)
|
|
assert.equal(value.totals.every(row => row.actual === row.quantity), true)
|
|
assert.equal(scope.page.computed.ready.call(value), true)
|
|
})
|
|
|
|
test('counting still rejects mismatched totals and invalid box quantities', () => {
|
|
for (const value of [state('1', '2', '2'), state('0', '4', '2'), state('1.5', '2.5', '2')]) {
|
|
assert.equal(scope.page.computed.ready.call(value), false)
|
|
}
|
|
const duplicate = state()
|
|
duplicate.task.boxes[0].items = [{ skuCode: 'A', quantity: 1 }, { skuCode: 'A', quantity: 1 }]
|
|
assert.equal(scope.page.computed.ready.call(duplicate), false)
|
|
})
|
|
|
|
function scanState() {
|
|
const value = { task: state().task, tasks: [], error: null, scanCode: '', scanResult: '', selectedMark: '', busy: false,
|
|
open(row) { this.task = row }, $nextTick(callback) { callback() } }
|
|
value.run = scope.page.methods.run.bind(value)
|
|
value.enterTask = scope.page.methods.enterTask.bind(value)
|
|
value.offerNextPalletTask = scope.page.methods.offerNextPalletTask.bind(value)
|
|
return value
|
|
}
|
|
|
|
test('box and single-batch pallet scans claim and open counting directly without start dialog', async () => {
|
|
for (const mark of ['RC#2-1', 'PLT-1']) {
|
|
const value = scanState()
|
|
scope.scanReturnCountTasks = async () => [{ receiptNo: 'RR-SCAN', returnNo: 'RT-SCAN' }]
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
let claimed
|
|
scope.beginReturnCountTask = async receiptNo => { claimed = receiptNo; assert.equal(value.busy, true); return { receiptNo, status: 'COUNTING' } }
|
|
value.ask = async () => { assert.fail('matching scan should enter directly') }
|
|
await scope.page.methods.scan.call(value, mark)
|
|
assert.equal(claimed, 'RR-SCAN')
|
|
assert.equal(value.task.status, 'COUNTING')
|
|
assert.equal(value.selectedMark, mark)
|
|
assert.equal(value.activeTask, value.task)
|
|
}
|
|
})
|
|
|
|
test('completed pallet offers remaining batches and starts only after confirmation', async () => {
|
|
for (const confirm of [false, true]) {
|
|
const value = scanState(); value.task = null
|
|
const next = { receiptNo: 'RR-NEXT', palletCode: 'PLT-1', status: 'PENDING' }
|
|
scope.scanReturnCountTasks = async code => { assert.equal(code, 'PLT-1'); return [next, { ...next, receiptNo: 'RR-DONE', status: 'CONFIRMED' }, { ...next, palletCode: 'PLT-2' }] }
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
let claimed = 0
|
|
scope.beginReturnCountTask = async receiptNo => { claimed++; assert.equal(receiptNo, 'RR-NEXT'); return { ...next, status: 'COUNTING' } }
|
|
value.ask = async options => { assert.match(options.content, /还有 1 项/); assert.equal(options.cancelText, '暂不清点'); return { confirm } }
|
|
await value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' })
|
|
assert.equal(claimed, confirm ? 1 : 0)
|
|
assert.equal(value.task?.status || null, confirm ? 'COUNTING' : null)
|
|
}
|
|
})
|
|
|
|
test('no remaining pallet task skips prompt and claim conflicts remain visible', async () => {
|
|
const value = scanState(); value.task = null
|
|
value.ask = async () => assert.fail('no remaining task should not prompt')
|
|
scope.scanReturnCountTasks = async () => []
|
|
await value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' })
|
|
scope.scanReturnCountTasks = async () => [{ receiptNo: 'RR-NEXT', palletCode: 'PLT-1', status: 'COUNTING' }]
|
|
value.ask = async () => ({ confirm: true })
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
scope.beginReturnCountTask = async () => { throw Object.assign(new Error('该任务正在由其他人员清点'), { code: 'A0409' }) }
|
|
await value.run(() => value.offerNextPalletTask({ receiptNo: 'RR-DONE', palletCode: 'PLT-1' }))
|
|
assert.equal(value.error.code, 'A0409')
|
|
assert.equal(value.task, null)
|
|
})
|
|
|
|
test('direct scan cannot claim a different task while the operator has unfinished work', async () => {
|
|
const value = scanState()
|
|
scope.scanReturnCountTasks = async () => [{ receiptNo: 'RR-NEW', returnNo: 'RT-NEW' }]
|
|
scope.getCurrentReturnCountTask = async () => ({ receiptNo: 'RR-OLD', returnNo: 'RT-OLD' })
|
|
scope.beginReturnCountTask = async () => { assert.fail('must not claim a second task') }
|
|
let dialog
|
|
value.ask = async options => { dialog = options; return { confirm: false } }
|
|
await scope.page.methods.scan.call(value, 'PLT-NEW')
|
|
assert.equal(dialog.title, '有未完成的清点任务')
|
|
})
|
|
|
|
test('a pallet scan shows every pending batch and no pending results clear stale details', async () => {
|
|
const value = scanState()
|
|
const rows = [{ receiptNo: 'RR-1', boxes: [] }, { receiptNo: 'RR-2', boxes: [] }]
|
|
scope.scanReturnCountTasks = async () => rows
|
|
await scope.page.methods.scan.call(value, 'PLT-1')
|
|
assert.equal(value.task, null)
|
|
assert.equal(value.tasks.length, 2)
|
|
assert.equal(value.scanResult, 'PLT-1')
|
|
scope.scanReturnCountTasks = async () => []
|
|
await scope.page.methods.scan.call(value, 'RC-DONE')
|
|
assert.equal(value.task, null)
|
|
assert.equal(value.tasks.length, 0)
|
|
assert.equal(value.scanResult, 'RC-DONE')
|
|
assert.equal(value.error, null)
|
|
})
|
|
|
|
test('scanner failures remain visible rather than claiming no count is required', async () => {
|
|
const value = scanState()
|
|
scope.scanReturnCountTasks = async () => { throw Object.assign(new Error('服务暂不可用'), { code: 'B0500' }) }
|
|
await scope.page.methods.scan.call(value, 'PLT-1')
|
|
assert.equal(value.error.code, 'B0500')
|
|
assert.equal(value.scanResult, '')
|
|
})
|
|
|
|
test('declining start never claims a task and another active task blocks a new claim', async () => {
|
|
const value = scanState(); value.activeTask = null
|
|
let begins = 0
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
scope.beginReturnCountTask = async () => { begins++; return { status: 'COUNTING' } }
|
|
value.ask = async () => ({ confirm: false })
|
|
await scope.page.methods.choose.call(value, { receiptNo: 'RR-NEW', returnNo: 'RT-NEW' })
|
|
assert.equal(begins, 0)
|
|
scope.getCurrentReturnCountTask = async () => ({ receiptNo: 'RR-OLD', returnNo: 'RT-OLD' })
|
|
let dialog
|
|
value.ask = async options => { dialog = options; return { confirm: false } }
|
|
await scope.page.methods.choose.call(value, { receiptNo: 'RR-NEW', returnNo: 'RT-NEW' })
|
|
assert.equal(dialog.title, '有未完成的清点任务')
|
|
assert.equal(begins, 0)
|
|
})
|
|
|
|
test('affirming start claims the selected task and switches it to working state', async () => {
|
|
const value = scanState(); value.scanResult = 'RC-1'
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
let claimed
|
|
scope.beginReturnCountTask = async receiptNo => { claimed = receiptNo; return { receiptNo, status: 'COUNTING' } }
|
|
value.ask = async () => ({ confirm: true })
|
|
await scope.page.methods.choose.call(value, { receiptNo: 'RR-NEW', returnNo: 'RT-NEW' })
|
|
assert.equal(claimed, 'RR-NEW')
|
|
assert.equal(value.task.status, 'COUNTING')
|
|
const unclaimed = state(); unclaimed.task.status = 'PENDING'
|
|
assert.equal(scope.page.computed.ready.call(unclaimed), false)
|
|
})
|
|
|
|
test('completed counting returns to page one and refreshes remaining tasks while busy', async () => {
|
|
const value = scanState(); value.ready = true; value.draftKey = 'count-draft'; value.page = 3; value.scanResult = 'PLT-1'
|
|
value.resetList = scope.page.methods.resetList.bind(value)
|
|
value.fetchTasks = scope.page.methods.fetchTasks.bind(value)
|
|
let removed = false, refreshed = false
|
|
scope.uni = { setStorageSync() {}, removeStorageSync() { removed = true }, showToast() {} }
|
|
scope.confirmReturnCount = async () => ({ status: 'CONFIRMED' })
|
|
scope.getCurrentReturnCountTask = async () => null
|
|
scope.listReturnCountTasks = async ({ page }) => { assert.equal(page, 1); assert.equal(value.busy, true); refreshed = true; return { records: [{ receiptNo: 'RR-NEXT' }], total: '1' } }
|
|
await scope.page.methods.confirm.call(value)
|
|
assert.equal(value.task, null)
|
|
assert.equal(value.scanResult, '')
|
|
assert.equal(value.activeTask, null)
|
|
assert.equal(value.tasks[0].receiptNo, 'RR-NEXT')
|
|
assert.equal(removed && refreshed, true)
|
|
})
|
|
|
|
test('failed confirmation retains the current task and draft for retry', async () => {
|
|
const value = scanState(); value.ready = true; value.draftKey = 'count-draft'
|
|
const original = value.task
|
|
let removed = false
|
|
scope.uni = { setStorageSync() {}, removeStorageSync() { removed = true } }
|
|
scope.confirmReturnCount = async () => { throw new Error('数量汇总不一致') }
|
|
await scope.page.methods.confirm.call(value)
|
|
assert.equal(value.task, original)
|
|
assert.equal(removed, false)
|
|
assert.equal(value.error.message, '数量汇总不一致')
|
|
})
|