119 lines
7.5 KiB
JavaScript
119 lines
7.5 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import vm from 'node:vm'
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
const source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
|
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
|
const noop = () => {}
|
|
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
|
|
function mount(overrides = {}, state = {}) {
|
|
const globals = { console, setTimeout, clearTimeout }
|
|
for (const match of script.matchAll(/import\s+(.*?)\s+from\s+['"][^'"]+['"]/g)) {
|
|
for (const name of match[1].replace(/[{}]/g, '').split(',').map(value => value.trim()).filter(Boolean)) globals[name] = noop
|
|
}
|
|
Object.assign(globals, { session: { token: 'offline', currentUser: { id: 'operator' } }, requireSession: () => true,
|
|
ITEM_STATUS: { PICKED: 'PICKED', PARTIAL: 'PARTIAL', SKIPPED: 'SKIPPED' },
|
|
isSealedPalletItem: () => false, isRemoteReady: () => true, stableOperationId: () => 'stable-operation',
|
|
stableTaskOperationIdentity: () => ({}), normalizePickingTask: value => value,
|
|
createItemStates: () => ({}), summarizeInventoryResources: () => ({}),
|
|
submitStates: () => ({ states: {}, autoSkippedIds: [] }), getPickSummary: () => ({}), ...overrides })
|
|
vm.createContext(globals)
|
|
vm.runInContext(script.replace(/^\s*import .*$/gm, '').replace('export default', 'globalThis.page ='), globals)
|
|
const page = globals.page
|
|
const ctx = { taskRequestId: 0, _pageVisible: true, evidenceContextId: 0, remoteTaskId: 'task',
|
|
currentBox: { taskId: 'task', warehouseId: 'warehouse', inboundId: 'inbound', loadingPlanId: 'plan', containerBookingId: 'container',
|
|
items: [{ id: 'item', taskItemId: 'item', itemStatus: 'PENDING', qty: 2, version: 0 }] },
|
|
evidenceFiles: {}, evidenceIdentity: null, sealNo: '', pickingVersion: 1,
|
|
summary: { pending: 1 }, itemStates: {}, partialQuantities: {}, completedBoxes: {},
|
|
selectedIncompleteReason: '现场未找到', submitting: false, loadingStageReady: false, pickingCompleted: false }
|
|
for (const [name, method] of Object.entries(page.methods)) ctx[name] = method.bind(ctx)
|
|
Object.assign(ctx, { rememberEvidenceDraft: noop, draftIdentity: () => ({}), resetEvidenceState: noop,
|
|
debugSubmit: noop, debugEvidence: noop, showToast: noop, persistSession: noop, clearTimers: noop,
|
|
stopHardwareScanner: noop, startHardwareScanner: noop, captureEvidenceIdentity: () => ({}),
|
|
restoreEvidenceDraft: noop, isSameEvidenceIdentity: () => false, restoreRemoteDraft: noop,
|
|
itemState: () => 'PENDING', saveCurrentBoxDraft: noop, openOverlay: noop,
|
|
isTaskConflict: () => false, remoteFailureMessage: () => 'failed', ...state })
|
|
return { page, ctx }
|
|
}
|
|
|
|
test('double confirmation and direct reentry send exactly one snapshot and one batch', async () => {
|
|
const snapshot = deferred(), batch = deferred(); let snapshots = 0, batches = 0
|
|
const { ctx } = mount({ submitContainerPickingBatch: async () => { batches++; return batch.promise } }, {
|
|
refreshInventorySnapshot: async () => { snapshots++; return snapshot.promise },
|
|
syncRemoteTask: async function () { this.pickingCompleted = true },
|
|
})
|
|
const first = ctx.confirmIncompleteSubmit()
|
|
ctx.confirmIncompleteSubmit(); await ctx.performSubmit('现场未找到')
|
|
assert.equal(ctx.submitting, true); assert.equal(snapshots, 1); assert.equal(batches, 0)
|
|
snapshot.resolve({ snapshotToken: 'snapshot' }); await new Promise(setImmediate)
|
|
ctx.confirmIncompleteSubmit(); assert.equal(batches, 1)
|
|
batch.resolve(null); await first
|
|
assert.equal(snapshots, 1); assert.equal(batches, 1); assert.equal(ctx.submitting, false)
|
|
assert.match(source, /:disabled="submitting \|\| !selectedIncompleteReason"/)
|
|
})
|
|
|
|
test('failed request releases submit guard and retry keeps operation identity', async () => {
|
|
const keys = []; let snapshots = 0
|
|
const { ctx } = mount({ submitContainerPickingBatch: async (_task, _actions, options) => { keys.push(options.operationId); throw new Error('network') } }, {
|
|
refreshInventorySnapshot: async () => { snapshots++; return { snapshotToken: 'snapshot' } },
|
|
})
|
|
await ctx.confirmIncompleteSubmit(); assert.equal(ctx.submitting, false)
|
|
await ctx.confirmIncompleteSubmit()
|
|
assert.equal(snapshots, 2); assert.deepEqual(keys, ['stable-operation', 'stable-operation'])
|
|
})
|
|
|
|
const task = () => ({ taskId: 'task', recordStatus: 'ACTIVE', loadingPlanId: 'plan', containerBookingId: 'container',
|
|
currentContainerNo: 'TEST', version: 1, workflowStatus: 'UNASSIGNED', items: [{ id: 'item', packageCount: 2, itemStatus: 'PENDING' }] })
|
|
for (const hook of ['onHide', 'onUnload']) {
|
|
test(`${hook} while detail is pending prevents claim and evidence requests`, async () => {
|
|
const detail = deferred(); let claims = 0, evidence = 0
|
|
const { page, ctx } = mount({ getContainerPickingTask: () => detail.promise,
|
|
claimContainerPickingTask: async () => { claims++; return {} } }, { loadLoadingEvidence: async () => { evidence++ } })
|
|
const pending = ctx.syncRemoteTask(); page[hook].call(ctx); detail.resolve(task()); await pending
|
|
assert.equal(claims, 0); assert.equal(evidence, 0); assert.equal(ctx.remoteLoading, false)
|
|
})
|
|
}
|
|
|
|
test('unload during already-sent claim prevents its response from loading evidence', async () => {
|
|
const claim = deferred(); let claims = 0, evidence = 0
|
|
const { page, ctx } = mount({ getContainerPickingTask: async () => task(),
|
|
claimContainerPickingTask: () => { claims++; return claim.promise } }, { loadLoadingEvidence: async () => { evidence++ } })
|
|
const pending = ctx.syncRemoteTask(); await new Promise(setImmediate)
|
|
assert.equal(claims, 1, ctx.remoteError)
|
|
page.onUnload.call(ctx); claim.resolve({ workflowStatus: 'IN_PROGRESS', version: 2 }); await pending
|
|
assert.equal(evidence, 0); assert.equal(ctx.remotePick, undefined)
|
|
})
|
|
|
|
test('unload during onShow list fetch prevents starting task sync afterward', async () => {
|
|
const list = deferred(); let syncs = 0
|
|
const { page, ctx } = mount({}, { _pageVisible: false, refreshAvailableBoxes: () => list.promise,
|
|
syncRemoteTask: async () => { syncs++ } })
|
|
const pending = page.onShow.call(ctx); page.onUnload.call(ctx); list.resolve(); await pending
|
|
assert.equal(syncs, 0)
|
|
})
|
|
|
|
test('old hidden onShow cannot clear a newer visible refresh or start another sync', async () => {
|
|
const oldList = deferred(), newList = deferred(); let lists = 0, syncs = 0
|
|
const { page, ctx } = mount({}, { _pageVisible: false,
|
|
refreshAvailableBoxes: () => (++lists === 1 ? oldList.promise : newList.promise),
|
|
syncRemoteTask: async () => { syncs++ } })
|
|
const oldShow = page.onShow.call(ctx)
|
|
page.onHide.call(ctx)
|
|
const newShow = page.onShow.call(ctx), currentPromise = ctx._showRefreshPromise
|
|
oldList.resolve(); await oldShow
|
|
assert.equal(syncs, 0); assert.equal(ctx._showRefreshPromise, currentPromise)
|
|
newList.resolve(); await newShow
|
|
assert.equal(syncs, 1); assert.equal(ctx._showRefreshPromise, null)
|
|
})
|
|
|
|
for (const mode of ['camera', 'preview']) {
|
|
test(`${mode} hide/return keeps existing exception and avoids refresh`, async () => {
|
|
let refreshes = 0
|
|
const { page, ctx } = mount({}, { cameraPicking: mode === 'camera', evidencePreviewOpening: mode === 'preview',
|
|
refreshAvailableBoxes: async () => { refreshes++ }, syncRemoteTask: async () => { refreshes++ } })
|
|
page.onHide.call(ctx); assert.equal(ctx.taskRequestId, 0)
|
|
await page.onShow.call(ctx); assert.equal(refreshes, 0); assert.equal(ctx._pageVisible, true)
|
|
})
|
|
}
|