Files
pda-uniapp/tests/pda-loading-api.test.mjs
T

182 lines
12 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../api/pda-loading.js', import.meta.url), 'utf8')
const http = await readFile(new URL('../api/http.js', import.meta.url), 'utf8')
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
test('sealed pallets use atomic batch actions and cannot use manual package actions', () => {
assert.doesNotMatch(source, /\/pallets\/pick/)
assert.match(source, /palletNo: action\.type === 'PICK_PALLET' \? action\.palletNo : null/)
assert.match(page, /type: 'PICK_PALLET', itemId: item\.taskItemId \?\? item\.id, palletNo: item\.palletNo/)
assert.match(page, /if \(isPalletItem\(result\.item\)\) return this\.pickScannedPallet\(result\.item\)/)
assert.match(page, /itemState\(item\) === ITEM_STATUS\.PENDING && !isPalletItem\(item\)/)
assert.match(page, /if \(isPalletItem\(item\)\) \{ this\.showToast\('请扫描托盘码整体拣出'/)
assert.match(page, /if \(isPalletItem\(item\)\) return/)
assert.match(page, /item\.palletNo \|\| '托盘码待同步'/)
})
test('a DP scan changes all same-pallet rows locally without any remote command', async () => {
const method = page.match(/pickScannedPallet\(item\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
assert.ok(method)
const stateSource = await readFile(new URL('../utils/picking-state.js', import.meta.url), 'utf8')
const { isPalletItem, ITEM_STATUS } = await import(`data:text/javascript;base64,${Buffer.from(stateSource).toString('base64')}`)
const undo = page.match(/undoPallet\(item\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
const behavior = new Function('isPalletItem', 'ITEM_STATUS', `return ({${method}${undo}})`)(isPalletItem, ITEM_STATUS)
let saved = 0
const rows = [{ id: '1', unitType: 'PALLET', palletNo: 'DP-260909-0143', qty: 2, itemStatus: 'PENDING' }, { id: '2', unitType: 'PALLET', palletNo: 'DP-260909-0143', qty: 3, itemStatus: 'PENDING' }, { id: '3', unitType: 'PACKAGE', qty: 1, itemStatus: 'PENDING' }]
const context = { submitting: false, requirePickingOperation: () => true, currentBox: { items: rows }, itemStates: {}, partialQuantities: {}, scannedBoxCodes: {}, recordScanResult() {}, showToast() {}, persistSession() { saved++ } }
behavior.pickScannedPallet.call(context, rows[0])
assert.deepEqual(context.itemStates, { 1: 'picked', 2: 'picked' })
assert.deepEqual(context.partialQuantities, { 1: 2, 2: 3 })
assert.equal(saved, 1)
behavior.undoPallet.call(context, rows[0])
assert.deepEqual(context.itemStates, { 1: 'pending', 2: 'pending' })
assert.deepEqual(context.scannedBoxCodes, {})
assert.equal(saved, 2)
assert.doesNotMatch(method, /await|pickContainer|syncRemote|request\(/)
})
test('PDA loading adapter uses only task-scoped cabinet evidence paths', () => {
for (const path of [
'/first-leg/pda-loading/tasks/pending'
]) assert.match(source, new RegExp(path.replace(/[?]/g, '\\?')))
assert.match(source, /first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence/)
assert.match(source, /'Idempotency-Key'/)
assert.match(source, /PHOTO_STEP_1[\s\S]*PHOTO_STEP_4[\s\S]*SEAL/)
for (const label of ['空仓照片', '半仓照片', '满仓照片', '锁仓照片', '带锁码照']) assert.match(source, new RegExp(label))
assert.match(source, /apiUpload\(/)
assert.match(http, /uni\.uploadFile/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\?\$\{query\.join\('&'\)\}/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/void/)
assert.doesNotMatch(source, /inbounds\/\$\{encodeURIComponent\(inboundId\)\}\/loading\/evidence/)
assert.doesNotMatch(source, /loading\/evidence\/pda\?tenantId=/)
assert.match(source, /export function completeContainerPickingTask\(taskId, payload = \{\}\)/)
assert.match(source, /first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/complete/)
assert.match(source, /expectedTaskVersion/)
})
test('PDA adapter does not treat non-200 response bodies or network failures as success', () => {
assert.match(http, /if \(code !== '200'\)/)
assert.match(http, /next\.network = true/)
assert.match(source, /throw new Error\('缺少装柜任务标识,无法上传装柜照片/)
})
test('C02 identifiers and separate picking/inbound versions are normalized explicitly', () => {
assert.match(source, /inboundId: row\.inboundId/)
assert.match(source, /inboundVersion: row\.inboundVersion/)
assert.match(source, /pickingVersion: row\.pickingVersion \?\? row\.loading\?\.pickingVersion/)
assert.match(source, /pickingVersion: task\.pickingVersion \?\? task\.version/)
assert.match(page, /version: Number\(this\.pickingVersion\)/)
assert.match(page, /ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates/)
assert.match(page, /taskItemId: item\.id \?\? item\.itemId/)
assert.match(page, /submit\.batch/)
assert.match(page, /item\.itemStatus === 'PICKED'/)
assert.match(page, /item\.itemStatus === 'PARTIAL'/)
assert.match(page, /!pickingCompleted && !summary\.canSubmit/)
assert.doesNotMatch(page, /loadingVersion/)
assert.match(page, /状态尚未同步,请刷新重试/)
})
test('PDA normalizes the frozen active and merged container identity fields', () => {
for (const field of ['sourceType', 'recordStatus', 'mergedIntoContainerBookingId']) {
assert.match(source, new RegExp(`${field}: row\\.${field} \\?\\? row\\.container\\?\\.${field}`))
assert.match(source, new RegExp(`${field}: task\\.${field} \\?\\? task\\.container\\?\\.${field}`))
}
})
test('picking page exposes five evidence slots, retry and remote completion states', () => {
for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成']) {
assert.match(`${source}\n${page}`, new RegExp(text))
}
assert.match(page, /createEvidenceUploadQueue/)
assert.match(page, /retryEvidence/)
assert.match(page, /uploadEvidenceFile/)
assert.match(page, /submitting \|\| \(!pickingCompleted && !summary\.canSubmit\)/)
assert.doesNotMatch(page, /completePicking\(/)
assert.match(page, /sourceType: \['camera'\]/)
assert.doesNotMatch(page, /sourceType: \['camera', 'album'\]/)
assert.match(page, /evidenceEntries\(slot\.type\)/)
assert.match(page, /voidEvidencePhoto\(/)
assert.match(page, /evidenceComplete/)
})
test('PDA evidence binding normalizes string ids from the task-scoped DTO', () => {
assert.match(page, /evidenceItems\(result\)[\s\S]*?id: String\(item\.evidenceId \?\? item\.id/)
assert.match(page, /fileId: String\(item\.fileId/)
assert.match(page, /applyEvidenceResponse\(result, type, localPath, identity\)[\s\S]*?this\.evidenceFiles = remoteItems[\s\S]*?\.reduce/)
assert.match(page, /cacheEvidencePreview\(saved\.fileId, localPath/)
})
test('PDA downloads authenticated task-scoped evidence previews', () => {
assert.match(source, /export function getEvidencePreviewUrl\(taskId, evidenceId, warehouseId\)/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/content\?warehouseId=/)
assert.match(source, /export function downloadEvidencePreview\(previewUrl\)/)
assert.match(source, /apiDownload\(\{ url \}\)/)
assert.match(http, /uni\.downloadFile\(/)
assert.match(http, /headers\.Authorization = `Bearer \$\{accessToken\}`/)
assert.match(source, /response\.tempFilePath/)
assert.match(source, /evidence\.preview\.response/)
assert.match(source, /\^image\\\//)
assert.match(source, /resolveEvidencePreviewUrl/)
assert.match(source, /const url = resolveEvidencePreviewUrl\(previewUrl, session\.apiBase/)
})
test('cabinet evidence never selects an inbound owner or builds a container route in the PDA', () => {
const evidenceAdapter = source.match(/export function getEvidence\(taskId, warehouseId\)[\s\S]*?export function completeContainerPickingTask/)?.[0] || ''
for (const field of ['inboundId', 'tenantId', 'containerBookingId']) assert.doesNotMatch(evidenceAdapter, new RegExp(field))
assert.match(page, /captureEvidenceIdentity\(\) \{[\s\S]*?taskId:/)
assert.match(page, /uploadEvidenceFile\(item\.taskId, item\.warehouseId, item, item\.localPath/)
assert.match(page, /queue\.add\(\{ taskId: identity\.taskId, warehouseId: identity\.warehouseId, evidenceType: type, expectedVersion/)
assert.doesNotMatch(page, /evidenceIdentity\?\.inboundId|identity\.inboundId/)
assert.doesNotMatch(page, /currentBox\.warehouseId \|\| session\.currentUser/)
assert.match(source, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
})
test('PDA cabinet picking adapter claims online and submits one atomic batch', () => {
assert.match(source, /\/first-leg\/pda-loading\/tasks\/pending/)
assert.doesNotMatch(source, /query\.push\(`cabinetNo=/)
assert.doesNotMatch(source, /query\.push\(`loadingPlanId=/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/inventory-snapshot\?warehouseId=/)
assert.match(source, /submitContainerPickingBatch/)
assert.match(source, /stableIdempotencyKey/)
assert.match(source, /Math\.imul\(hash, 16777619\)/)
assert.match(source, /slice\(0, 128\)/)
assert.match(page, /claimContainerPickingTask\(/)
assert.match(page, /submitContainerPickingBatch\(/)
assert.match(page, /任务版本已变化,请刷新后重试/)
assert.match(source, /data: \{ warehouseId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId, expectedVersion, inventorySnapshotToken, items \}/)
assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /releaseMode|locationIds|reservationIds/)
assert.doesNotMatch(source, /query\.push\(`containerNo=/)
assert.doesNotMatch(page, /getContainerPickingTask\([^\n]*containerNo:/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\?warehouseId=/)
assert.match(source, /currentContainerNo/)
assert.match(source, /getPickingInventorySnapshot[\s\S]*?\.then\(normalizeInventorySnapshot\)/)
assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /containerNo/)
assert.doesNotMatch(page, /claim-[^`]*currentBox\.boxNo/)
assert.doesNotMatch(page.match(/stableOperationId\('submit'[\s\S]*?\n/)?.[0] || '', /boxNo|containerNo|currentContainerNo/)
assert.match(page, /await getPickingInventorySnapshot\([\s\S]*await submitContainerPickingBatch/)
assert.match(page, /库存已变化,请刷新后重试/)
assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /for \(/)
assert.match(page, /version: Number\(item\.version \|\| 0\)/)
assert.match(page, /refreshTask/)
assert.match(page, /remoteError && !pickingCompleted/)
assert.match(page, /库存快照未同步,请刷新任务后重试/)
assert.match(page, /拣货完成未提交,请查看错误提示/)
assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\)[\s\S]*claimContainerPickingTask/)
assert.doesNotMatch(page, /loadingVersion/)
assert.match(page, /taskRequestId/)
assert.match(page, /resetEvidenceState/)
assert.match(page, /availablePickQuantity/)
assert.match(page, /库存\/库位可用量不足/)
})
test('PDA final completion delegates automatic cabinet side effects to the task complete endpoint', () => {
const complete = source.match(/export function completeContainerPickingTask\(taskId, payload = \{\}\) \{[\s\S]*?\n\}/)?.[0] || ''
assert.match(complete, /\/first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/complete/)
assert.doesNotMatch(complete, /拖车|trucking/i)
})