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

204 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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')
const styles = await readFile(new URL('../styles/pda.scss', import.meta.url), 'utf8')
test('sealed pallets use a single authoritative pallet command and prohibit partial picking', () => {
assert.match(source, /pallets\/pick/)
assert.match(page, /submitImmediateItemAction\(item, 'PICK_PALLET', \{ palletNo: item.palletNo \}\)/)
assert.match(page, /整托货物不能跳拣/)
assert.match(page, /整托货物不能部分拣货/)
assert.match(page, /expectedTaskVersion: Number\(this.pickingVersion\)/)
})
test('DP scan records success only after the atomic server command succeeds', async () => {
const method = page.match(/async pickScannedPallet\(item, palletNo\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
assert.ok(method)
const behavior = new Function('ITEM_STATUS', 'return ({' + method + '})')({ PICKED: 'picked' })
const item = { id: '1', palletNo: 'DP-260914-0001', qty: 5 }
const calls = []
let succeeds = false
const context = { itemStates: {}, partialQuantities: {}, scannedBoxCodes: {}, async submitImmediateItemAction(...args) { calls.push(args); return succeeds }, recordScanResult() {}, showToast() {}, persistSession() {} }
assert.equal(await behavior.pickScannedPallet.call(context, item, item.palletNo), false)
assert.deepEqual(context.itemStates, {})
succeeds = true
assert.equal(await behavior.pickScannedPallet.call(context, item, item.palletNo), true)
assert.deepEqual(context.itemStates, { 1: 'picked' })
assert.deepEqual(calls[0], [item, 'PICK_PALLET', { palletNo: item.palletNo }])
assert.deepEqual(calls[0], calls[1])
})
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\/warehouses/)
assert.match(source, /query\.push\(`warehouseId=\$\{encodeURIComponent\(warehouseId\)\}`\)/)
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\.version \?\? task\.pickingVersion/)
assert.match(page, /version: Number\(this\.pickingVersion\)/)
assert.match(page, /ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, 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, /deleteEvidencePhoto\(/)
assert.match(page, /class="evidence-delete-btn" aria-label="删除照片"[\s\S]*?>×<\/button>/)
assert.doesNotMatch(page, />作废<\/button>/)
assert.match(styles, /\.evidence-delete-btn\s*\{[^}]*width:\s*40px;[^}]*background:\s*transparent;/)
assert.match(page, /evidenceComplete/)
})
test('immediate task responses replace a stale normalized picking version', () => {
const normalizeSource = source.match(/export function normalizePickingTask\(task = \{\}\) \{[\s\S]*?\n\}/)?.[0]
assert.ok(normalizeSource)
const normalizePickingTask = new Function(`${normalizeSource.replace('export function', 'function')}; return normalizePickingTask`)()
const previous = normalizePickingTask({ id: 'task-1', version: 3 })
assert.equal(previous.pickingVersion, 3)
const mergedMutationResponse = normalizePickingTask({ ...previous, version: 4 })
assert.equal(mergedMutationResponse.pickingVersion, 4)
})
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\(taskId\)\}\/items\/\$\{encodeURIComponent\(itemId\)\}\/\$\{path\}/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/pallets\/pick/)
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, /submitImmediateItemAction\(item, 'PICK'/)
assert.match(page, /submitImmediateItemAction\(item, 'PARTIAL'/)
assert.match(page, /submitImmediateItemAction\(item, 'SKIP'/)
assert.match(page, /submitImmediateItemAction\(item, 'PICK_PALLET'/)
assert.match(page, /item\.itemStatus === 'PICKING' \|\| item\.itemStatus === 'PARTIAL'/)
assert.doesNotMatch(page, /@tap="undoItem\(item\.id\)"/)
assert.match(page, /submitContainerPickingBatch\(/)
assert.match(source, /palletNo: action\.type === 'PICK_PALLET' \? action\.palletNo : null/)
assert.match(page, /type: 'PICK_PALLET'[\s\S]*?palletNo: item\.palletNo/)
assert.match(page, /扫描托盘码[\s\S]*?托内任一箱码/)
assert.match(page, /确认整托拣出/)
assert.match(page, /async pickItem[\s\S]*?isSealedPalletItem\(item\)[\s\S]*?submitImmediateItemAction\(item, 'PICK_PALLET', \{ palletNo: item\.palletNo \}\)/)
assert.match(page, /或点击“确认整托拣出”后再提交/)
assert.match(page, /sanitizeUserMessage\(error\.message, '任务状态已变化,请刷新后重试'\)/)
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)
})