diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index ca65818..dbd1bcf 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -117,7 +117,7 @@ - + 装柜证据空仓、半仓、满仓、锁仓及带锁码照 {{ remoteSyncState.label }} @@ -141,8 +141,11 @@ {{ remoteError }} - - 等待统一订单详情提供装柜版本号后可提交完成 + + + 状态尚未同步,请刷新重试 + + @@ -332,6 +335,16 @@ import ScanTargetInput from '@/components/ScanTargetInput.vue' import PopupHeader from '@/components/PopupHeader.vue' + const VERSION_SYNC_MESSAGE = '状态尚未同步,请刷新重试' + + function isPickingCompleteTask(task) { + const workflowStatus = task?.workflowStatus + const taskStatus = task?.taskStatus ?? task?.status + return workflowStatus === 'AWAITING_REVIEW' + || workflowStatus === 'COMPLETED' + || taskStatus === 'COMPLETED' + } + export default { components: { PopupHeader, ScanTargetInput }, data() { @@ -354,8 +367,9 @@ selectedIncompleteReason: '', completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 }, evidenceSlots: EVIDENCE_SLOTS, - evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null, + evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null, remoteTaskId: null, assignmentVersion: null, submitId: '', + taskRequestId: 0, remoteError: '', remoteLoading: false, submitting: false, toast: { visible: false, message: '', type: '' } } @@ -388,6 +402,10 @@ pendingBoxCount() { return this.boxes.length - this.completedCount }, + pickingCompleted() { + return Boolean(this.completedBoxes[this.currentBoxIndex]) + || isPickingCompleteTask(this.remotePick) + }, partialTargetItem() { return this.currentBox.items.find((item) => item.id === this.partialTargetId) || null }, @@ -446,10 +464,7 @@ } }, created() { - this.evidenceQueue = createEvidenceUploadQueue({ - upload: (item, onProgress) => uploadEvidenceFile(item.inboundId, item.evidenceType, item.localPath, item.tenantId, onProgress), - onChange: (items) => { this.evidenceQueueItems = items } - }) + this.resetEvidenceState() }, onLoad(options = {}) { if (!requireSession()) return @@ -481,6 +496,26 @@ return false }, methods: { + isEvidenceContextActive(contextId, inboundId) { + return contextId === this.evidenceContextId + && String(this.currentBox.inboundId ?? '') === String(inboundId ?? '') + }, + requireEvidenceStage() { + if (this.pickingCompleted) return true + this.showToast('请先提交拣货完成', 'warning') + return false + }, + resetEvidenceState() { + const contextId = this.evidenceContextId + 1 + this.evidenceContextId = contextId + this.evidenceFiles = {} + this.evidenceQueueItems = [] + this.sealNo = '' + this.evidenceQueue = createEvidenceUploadQueue({ + upload: (item, onProgress) => uploadEvidenceFile(item.inboundId, item.evidenceType, item.localPath, item.tenantId, onProgress), + onChange: (items) => { if (this.evidenceContextId === contextId) this.evidenceQueueItems = items } + }) + }, normalizeScanHistory(entries) { if (!Array.isArray(entries)) return [] return entries.map((entry) => { @@ -598,11 +633,15 @@ }, selectBox(index) { this.saveCurrentBoxDraft() + this.taskRequestId += 1 + this.submitting = false this.currentBoxIndex = index this.remoteTaskId = null this.remotePick = null this.pickingVersion = null + this.loadingVersion = null this.assignmentVersion = null + this.resetEvidenceState() this.boxDropdownOpen = false this.loadBoxState(index) this.scrollToTop() @@ -670,11 +709,13 @@ }, async syncRemoteTask() { if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return + const requestId = ++this.taskRequestId this.remoteLoading = true this.remoteError = '' try { const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse - const task = await getContainerPickingTask({ taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId, containerNo: this.currentBox.currentContainerNo }, warehouseId) + const task = await getContainerPickingTask({ taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId }, warehouseId) + if (requestId !== this.taskRequestId) return if (!task) throw new Error('未找到当前柜次的拣货任务') const remoteItems = task.items || task.pickItems || task.details || task.lines if (Array.isArray(remoteItems) && remoteItems.length) { @@ -710,22 +751,38 @@ this.currentBox.cabinetNo = task.cabinetNo this.currentBox.boxNo = task.cabinetNo this.currentBox.currentContainerNo = task.currentContainerNo - const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}` - const claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId) + const remoteTask = normalizePickingTask(task) + let claimed = null + if (!isPickingCompleteTask(remoteTask)) { + const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}` + claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId) + if (requestId !== this.taskRequestId) return + } this.remotePick = normalizePickingTask({ ...task, ...(claimed || {}) }) this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null this.loadingVersion = this.currentBox.inboundVersion ?? this.remotePick.inboundVersion ?? null this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion this.restoreRemoteDraft() - if (this.currentBox.inboundId) { - const evidence = await getEvidence(this.currentBox.inboundId, this.currentBox.tenantId) - this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {}) - const seal = this.evidenceEntry('SEAL') - if (seal) this.sealNo = seal.sealNo || '' - } + this.resetEvidenceState() + if (this.pickingCompleted) await this.loadLoadingEvidence() } catch (error) { + if (requestId !== this.taskRequestId) return this.remoteError = error.network ? '网络不可用,无法开始拣货,恢复网络后请重试' : (error.message || '任务同步失败') - } finally { this.remoteLoading = false } + } finally { if (requestId === this.taskRequestId) this.remoteLoading = false } + }, + async loadLoadingEvidence() { + if (!this.pickingCompleted || !this.currentBox.inboundId || !isRemoteReady(this.currentBox.inboundId)) return + const contextId = this.evidenceContextId + try { + const evidence = await getEvidence(this.currentBox.inboundId, this.currentBox.tenantId) + if (contextId !== this.evidenceContextId) return + this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {}) + const seal = this.evidenceEntry('SEAL') + this.sealNo = seal?.sealNo || '' + } catch (error) { + if (contextId !== this.evidenceContextId) return + this.remoteError = error.network ? '网络失败,装柜证据未加载,请刷新重试' : (error.message || '装柜证据加载失败') + } }, async refreshTask() { if (!this.currentBox.boxNo) return @@ -739,73 +796,118 @@ return this.evidenceEntry(type) ? '继续拍照' : '拍照' }, chooseEvidence(slot) { + if (!this.requireEvidenceStage()) return if (!isOnline()) { this.showToast('当前网络不可用,请联网后再拍照上传', 'warning'); return } if (typeof uni.chooseImage !== 'function') { this.showToast('当前环境不支持选择照片', 'warning'); return } + const contextId = this.evidenceContextId + const inboundId = this.currentBox.inboundId uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera'], success: (result) => { + if (!this.isEvidenceContextActive(contextId, inboundId)) return const path = result.tempFilePaths && result.tempFilePaths[0] if (!path) return const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' } this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] } this.queueEvidenceUpload(slot.type, path) - }, fail: () => this.showToast('未选择照片,可稍后重试', 'warning') }) + }, fail: () => { + if (!this.isEvidenceContextActive(contextId, inboundId)) return + this.showToast('未选择照片,可稍后重试', 'warning') + } }) }, async queueEvidenceUpload(type, path) { + if (!this.requireEvidenceStage()) return if (!this.currentBox.inboundId || !isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return } - const entry = this.evidenceQueue.add({ inboundId: this.currentBox.inboundId, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path }) - await this.evidenceQueue.flush() - const latest = this.evidenceQueue.snapshot().find((item) => item.id === entry.id) + const contextId = this.evidenceContextId + const queue = this.evidenceQueue + const entry = queue.add({ inboundId: this.currentBox.inboundId, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path }) + await queue.flush() + if (contextId !== this.evidenceContextId || queue !== this.evidenceQueue) return + const latest = queue.snapshot().find((item) => item.id === entry.id) if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return } const current = this.evidenceEntry(type) || {} this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] } await this.saveEvidence(type) }, async retryEvidence(type) { + if (!this.requireEvidenceStage()) return const item = this.evidenceQueueEntry(type) - if (!item || !this.evidenceQueue.retry(item.id)) return + const contextId = this.evidenceContextId + const queue = this.evidenceQueue + if (!item || !queue.retry(item.id)) return this.remoteError = '' - await this.evidenceQueue.flush() - const latest = this.evidenceQueue.latest(type) + await queue.flush() + if (contextId !== this.evidenceContextId || queue !== this.evidenceQueue) return + const latest = queue.latest(type) if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return } const current = this.evidenceEntry(type) || {} this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] } await this.saveEvidence(type) }, async saveEvidence(type, operationId = null) { + if (!this.requireEvidenceStage()) return if (!this.currentBox.inboundId) { this.showToast('演示任务已记录本地证据', 'success'); return } if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return } + const contextId = this.evidenceContextId + const inboundId = this.currentBox.inboundId + const tenantId = this.currentBox.tenantId const current = this.evidenceEntry(type) || {} if (!current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return } const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null, operationId: operationId || `evidence-${this.remoteTaskId || this.currentBox.inboundId}-${type}-${current.fileId || 'seal'}` } try { - const result = await upsertEvidence(this.currentBox.inboundId, payload, this.currentBox.tenantId) + const result = await upsertEvidence(inboundId, payload, tenantId) + if (!this.isEvidenceContextActive(contextId, inboundId)) return this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles) this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success') - } catch (error) { this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败'); this.showToast('证据保存失败', 'error') } + } catch (error) { + if (!this.isEvidenceContextActive(contextId, inboundId)) return + this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败') + this.showToast('证据保存失败', 'error') + } }, async saveSealNo() { + if (!this.requireEvidenceStage()) return if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return } await this.saveEvidence('SEAL', `seal-no-${this.remoteTaskId || this.currentBox.inboundId}-${Date.now()}`) }, async voidEvidencePhoto(photo) { + if (!this.requireEvidenceStage()) return if (!photo?.id || !this.currentBox.inboundId) return + const contextId = this.evidenceContextId + const inboundId = this.currentBox.inboundId + const tenantId = this.currentBox.tenantId const reason = 'PDA现场标记错图' try { - await voidEvidence(this.currentBox.inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${this.currentBox.inboundId}-${photo.id}-${photo.version || 0}` }, this.currentBox.tenantId) + await voidEvidence(inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${inboundId}-${photo.id}-${photo.version || 0}` }, tenantId) + if (!this.isEvidenceContextActive(contextId, inboundId)) return this.evidenceFiles = { ...this.evidenceFiles, [photo.evidenceType]: (this.evidenceFiles[photo.evidenceType] || []).map((item) => item.id === photo.id ? { ...item, status: 'VOID', voidReason: reason } : item) } this.showToast('照片已作废,可重新拍摄', 'success') } catch (error) { + if (!this.isEvidenceContextActive(contextId, inboundId)) return this.remoteError = error.network ? '网络失败,照片未作废,请重试' : (error.message || '照片作废失败') this.showToast('照片作废失败', 'error') } }, async completeLoadingTask() { - if (!isRemoteReady(this.currentBox.inboundId) || this.loadingVersion === null) return - if (!this.completedBoxes[this.currentBoxIndex]) { this.showToast('请先提交拣货完成', 'warning'); return } + if (!this.requireEvidenceStage()) return + if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('装柜任务尚未同步,请刷新后重试', 'warning'); return } + if (this.loadingVersion === null) { this.showToast(VERSION_SYNC_MESSAGE, 'warning'); return } if (!this.evidenceComplete) { this.showToast('五类有效照片和封签号均为必填', 'warning'); return } + const contextId = this.evidenceContextId + const inboundId = this.currentBox.inboundId + const tenantId = this.currentBox.tenantId + const loadingVersion = this.loadingVersion this.submitting = true - try { await completeLoading(this.currentBox.inboundId, { expectedVersion: this.loadingVersion, remark: 'PDA确认装柜完成' }, this.currentBox.tenantId); this.showToast('已确认装柜完成', 'success') } - catch (error) { this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败'); this.showToast('装柜完成失败', 'error') } - finally { this.submitting = false } + try { + await completeLoading(inboundId, { expectedVersion: loadingVersion, remark: 'PDA确认装柜完成' }, tenantId) + if (!this.isEvidenceContextActive(contextId, inboundId)) return + this.showToast('已确认装柜完成', 'success') + } catch (error) { + if (!this.isEvidenceContextActive(contextId, inboundId)) return + this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败') + this.showToast('装柜完成失败', 'error') + } + finally { + if (this.isEvidenceContextActive(contextId, inboundId)) this.submitting = false + } }, pickScannedBox(item, boxCode) { const pickedCount = Number(this.partialQuantities[item.id] || 0) + 1 @@ -943,7 +1045,7 @@ this.submitting = true try { if (!this.remoteTaskId) throw new Error('尚未进入服务端拣货任务,请联网刷新后重试') - if (this.pickingVersion === null) throw new Error('缺少拣货版本号 pickingVersion,无法提交') + if (this.pickingVersion === null) throw new Error(VERSION_SYNC_MESSAGE) const operationIdentity = stableTaskOperationIdentity(this.draftIdentity()) const submitId = this.submitId || stableOperationId('submit', operationIdentity, { itemStates: this.itemStates, partialQuantities: this.partialQuantities, incompleteReason }) this.submitId = submitId @@ -958,7 +1060,8 @@ if (incompleteReason) return { type: 'SKIP', itemId, reason: `${incompleteReason}(提交时确认未拣)`, version: Number(item.version || 0) } return null }).filter(Boolean) - await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken }) + const submittedTask = await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken }) + if (submittedTask) this.remotePick = normalizePickingTask({ ...this.remotePick, ...submittedTask }) // Legacy inbound completePicking(expectedVersion: Number(this.pickingVersion)) remains adapter-only; cabinet tasks use item actions. } catch (error) { const versionConflict = error.code === 'A0409' || error.statusCode === 409 @@ -979,6 +1082,7 @@ item.finalPartialQuantity = this.partialQuantities[item.id] || 0 }) this.completedBoxes = { ...this.completedBoxes, [this.currentBoxIndex]: true } + this.loadLoadingEvidence() this.saveCurrentBoxDraft() if (remoteRequired) clearPickingDraft(this.draftIdentity()) this.submitId = '' diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs index cc45cc6..3267b91 100644 --- a/tests/pda-loading-api.test.mjs +++ b/tests/pda-loading-api.test.mjs @@ -36,7 +36,7 @@ test('C02 identifiers and separate picking/inbound versions are normalized expli assert.match(source, /pickingVersion: task\.pickingVersion \?\? task\.version/) assert.match(page, /expectedVersion: Number\(this\.pickingVersion\)/) assert.match(page, /this\.loadingVersion = this\.currentBox\.inboundVersion/) - assert.match(page, /缺少拣货版本号 pickingVersion/) + assert.match(page, /状态尚未同步,请刷新重试/) }) test('picking page exposes five evidence slots, retry and remote completion states', () => { @@ -71,6 +71,8 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(source, /data: \{ warehouseId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId, expectedVersion, inventorySnapshotToken, items \}/) assert.match(source, /cabinetNo=/) assert.match(source, /loadingPlanId=/) + 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\)/) @@ -82,6 +84,9 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( 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, /if \(!isPickingCompleteTask\(remoteTask\)\)[\s\S]*claimContainerPickingTask/) + assert.match(page, /taskRequestId/) + assert.match(page, /resetEvidenceState/) assert.match(page, /availablePickQuantity/) assert.match(page, /库存\/库位可用量不足/) }) diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs new file mode 100644 index 0000000..aed7e88 --- /dev/null +++ b/tests/picking-evidence-stage.test.mjs @@ -0,0 +1,77 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' + +const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') + +test('loading evidence is mounted only after the current picking task is complete', () => { + assert.match(page, /v-if="currentBox\.inboundId && pickingCompleted" class="loading-evidence-section"/) + assert.match(page, /isPickingCompleteTask\(task\)[\s\S]*?AWAITING_REVIEW[\s\S]*?COMPLETED/) + assert.match(page, /pickingCompleted\(\)[\s\S]*?completedBoxes\[this\.currentBoxIndex\][\s\S]*?isPickingCompleteTask\(this\.remotePick\)/) + assert.doesNotMatch(page, /class="loading-evidence-section" v-if="currentBox\.inboundId"/) +}) + +test('loading evidence is not requested before picking completion', () => { + assert.match(page, /if \(this\.pickingCompleted\) await this\.loadLoadingEvidence\(\)/) + const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(loader, /if \(!this\.pickingCompleted/) + assert.match(loader, /await getEvidence\(/) + assert.match(page, /this\.completedBoxes = \{[\s\S]*?this\.loadLoadingEvidence\(\)/) +}) + +test('operators receive actionable refresh guidance without internal version wording', () => { + assert.match(page, /状态尚未同步,请刷新重试/) + assert.doesNotMatch(page, /缺少拣货版本号|提供装柜版本号/) + const complete = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(complete, /if \(!this\.requireEvidenceStage\(\)\) return/) +}) + +test('evidence mutations defensively reject calls before picking completion', () => { + assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(this\.pickingCompleted\) return true[\s\S]*?请先提交拣货完成/) + for (const method of ['chooseEvidence', 'queueEvidenceUpload', 'retryEvidence', 'saveEvidence', 'saveSealNo', 'voidEvidencePhoto', 'completeLoadingTask']) { + const body = page.match(new RegExp(`(?:async )?${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || '' + assert.match(body, /if \(!this\.requireEvidenceStage\(\)\) return/, `${method} should guard the evidence stage`) + } +}) + +test('switching cabinets invalidates old sync and evidence upload state', () => { + const selector = page.match(/selectBox\(index\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(selector, /this\.taskRequestId \+= 1/) + assert.match(selector, /this\.pickingVersion = null/) + assert.match(selector, /this\.loadingVersion = null/) + assert.match(selector, /this\.submitting = false/) + assert.match(selector, /this\.resetEvidenceState\(\)/) + assert.match(page, /contextId !== this\.evidenceContextId \|\| queue !== this\.evidenceQueue/) + const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.equal((loader.match(/contextId !== this\.evidenceContextId/g) || []).length, 2) +}) + +test('camera callbacks cannot append evidence after switching cabinets', () => { + const chooser = page.match(/chooseEvidence\(slot\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(chooser, /const contextId = this\.evidenceContextId/) + assert.match(chooser, /const inboundId = this\.currentBox\.inboundId/) + assert.equal((chooser.match(/isEvidenceContextActive\(contextId, inboundId\)/g) || []).length, 2) + assert.match(chooser, /isEvidenceContextActive\(contextId, inboundId\)[\s\S]*?this\.evidenceFiles =/) +}) + +test('evidence save, void and loading completion ignore stale cabinet responses', () => { + for (const [method, awaitedCall] of [ + ['saveEvidence', 'upsertEvidence'], + ['voidEvidencePhoto', 'voidEvidence'], + ['completeLoadingTask', 'completeLoading'] + ]) { + const body = page.match(new RegExp(`async ${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || '' + assert.match(body, /const contextId = this\.evidenceContextId/, `${method} should capture the evidence context`) + assert.match(body, /const inboundId = this\.currentBox\.inboundId/, `${method} should capture stable inbound identity`) + assert.match(body, new RegExp(`await ${awaitedCall}\\([\\s\\S]*?if \\(!this\\.isEvidenceContextActive\\(contextId, inboundId\\)\\) return`), `${method} should guard the success writeback`) + assert.ok((body.match(/isEvidenceContextActive\(contextId, inboundId\)/g) || []).length >= 2, `${method} should guard success and error writebacks`) + } + const complete = page.match(/async completeLoadingTask\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(complete, /finally \{[\s\S]*?isEvidenceContextActive\(contextId, inboundId\)[\s\S]*?this\.submitting = false/) +}) + +test('completed server tasks restore evidence stage without being claimed again', () => { + assert.match(page, /const remoteTask = normalizePickingTask\(task\)/) + assert.match(page, /if \(!isPickingCompleteTask\(remoteTask\)\) \{[\s\S]*?claimContainerPickingTask/) + assert.match(page, /if \(this\.pickingCompleted\) await this\.loadLoadingEvidence\(\)/) +})