123 lines
8.1 KiB
JavaScript
123 lines
8.1 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion } from '../utils/loading-completion-state.js'
|
|
|
|
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
|
const method = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)[0]
|
|
function completion(overrides = {}, savedAttempt = null) {
|
|
let attempt = savedAttempt
|
|
const calls = [], cleared = [], messages = []
|
|
const bindings = {
|
|
COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion,
|
|
isRemoteReady: () => true, stableTaskOperationIdentity: () => 'task', stableOperationId: (_action, _identity, payload) => `complete-v${payload.expectedTaskVersion}`,
|
|
restoreLoadingCompletionAttempt: () => attempt, saveLoadingCompletionAttempt: (_identity, value) => { attempt = value },
|
|
clearLoadingCompletionAttempt: identity => { cleared.push(identity); attempt = null },
|
|
completeContainerPickingTask: async (_task, payload) => { calls.push(payload); return { taskStatus: 'COMPLETED' } },
|
|
normalizePickingTask: task => task, observeLoadingCompletion: async options => options.initialTask,
|
|
sanitizeUserMessage: value => value, ...overrides
|
|
}
|
|
const behavior = new Function(...Object.keys(bindings), `return ({${method}})`)(...Object.values(bindings))
|
|
const state = {
|
|
evidenceIdentity: { taskId: 'task', warehouseId: 'warehouse' }, evidenceContextId: 1, remoteTaskId: 'task', remotePick: {},
|
|
pickingVersion: 1, evidenceComplete: true, submitting: false, _pageVisible: true,
|
|
requireEvidenceStage() { return !this.submitting }, draftIdentity: () => ({ taskId: 'task', loadingPlanId: 'plan', containerBookingId: 'cabinet' }),
|
|
isEvidenceContextActive(id) { return id === this.evidenceContextId }, isTaskConflict: error => error.code === 'A0409',
|
|
async refreshAfterTaskConflict() { this.pickingVersion = 2 }, debugSubmit() {}, persistSession() {},
|
|
remoteFailureMessage: error => error.message, showToast: message => messages.push(message)
|
|
}
|
|
state.complete = behavior.completeLoadingTask.bind(state)
|
|
return { state, calls, cleared, messages, attempt: () => attempt }
|
|
}
|
|
|
|
test('completion in flight blocks a second click and releases busy after the task context changes', async () => {
|
|
let resolve
|
|
const { state, calls } = completion({ completeContainerPickingTask: (...args) => { calls.push(args); return new Promise(done => { resolve = done }) } })
|
|
const first = state.complete(); await state.complete(); assert.equal(calls.length, 1)
|
|
state.evidenceContextId++; resolve({ taskStatus: 'COMPLETED' }); await first
|
|
assert.equal(state.submitting, false); assert.equal(state.remotePick.taskStatus, undefined)
|
|
})
|
|
|
|
test('an old completion response cannot release a newer picking submission', async () => {
|
|
let resolve
|
|
const { state } = completion({ completeContainerPickingTask: () => new Promise(done => { resolve = done }) })
|
|
const first = state.complete()
|
|
state.evidenceContextId++; state._submissionOwner = {}; state.submitting = true
|
|
resolve({ taskStatus: 'COMPLETED' }); await first
|
|
assert.equal(state.submitting, true)
|
|
})
|
|
|
|
test('timeout unlocks completion and the next click uses the current version', async () => {
|
|
const commands = []
|
|
const { state, attempt } = completion({ completeContainerPickingTask: async (_task, payload) => {
|
|
commands.push(payload); if (commands.length === 1) throw { network: true }; return { taskStatus: 'COMPLETED' }
|
|
} })
|
|
await state.complete(); assert.equal(state.submitting, false); assert.equal(attempt(), null)
|
|
state.pickingVersion = 7; await state.complete()
|
|
assert.equal(commands[1].expectedTaskVersion, 7); assert.notEqual(commands[0].operationId, commands[1].operationId)
|
|
})
|
|
|
|
test('first definite conflict releases its stale completion version so the next operation can succeed', async () => {
|
|
const commands = []
|
|
const { state, cleared } = completion({ completeContainerPickingTask: async (_task, payload) => {
|
|
commands.push(payload); if (commands.length === 1) throw { code: 'A0409', message: '任务已更新' }; return { taskStatus: 'COMPLETED' }
|
|
} })
|
|
await state.complete(); assert.equal(state.submitting, false); assert.equal(cleared.length, 0)
|
|
await state.complete(); assert.equal(commands[1].expectedTaskVersion, 2); assert.notEqual(commands[0].operationId, commands[1].operationId)
|
|
})
|
|
|
|
test('old local completion snapshots are ignored and cannot freeze the current task', async () => {
|
|
const old = { operationId: 'old', expectedTaskVersion: 1 }
|
|
const { state, calls } = completion({}, old)
|
|
state.pickingVersion = 8; await state.complete()
|
|
assert.equal(calls[0].expectedTaskVersion, 8); assert.notEqual(calls[0].operationId, old.operationId)
|
|
assert.equal(state.submitting, false)
|
|
})
|
|
|
|
test('a reopened processing task has an explicit refresh action without a mutation retry', () => {
|
|
assert.match(page, /v-if="evidenceReadOnly && loadingCompletionState.phase !== 'COMPLETED'"[^>]*@tap="refreshTask">刷新完成状态/)
|
|
})
|
|
|
|
test('photo failure releases the page and another click uploads the current photo with a fresh key', async () => {
|
|
const uploadMethod = page.slice(page.indexOf('async queueEvidenceUpload('), page.indexOf('async saveSealNo('))
|
|
const requests = []
|
|
const uploadEvidenceFile = async (...args) => { requests.push(args); if (requests.length === 1) throw { network: true, message: 'timeout' }; return { saved: true } }
|
|
const behavior = new Function('isRemoteReady', 'uploadEvidenceFile', 'return ({' + uploadMethod + '})')(() => true, uploadEvidenceFile)
|
|
const state = { evidenceUploading: false, evidenceReadOnly: false, evidenceIdentity: { taskId: 'task', warehouseId: 'warehouse' }, evidenceContextId: 1, evidenceQueueItems: [], remoteError: '', validEvidenceEntries: () => [], isEvidenceContextActive: () => true, remoteFailureMessage: error => error.message, applyEvidenceResponse() {}, showToast() {} }
|
|
await behavior.queueEvidenceUpload.call(state, 'SEAL', 'old.jpg', 'OLD')
|
|
assert.equal(state.evidenceUploading, false); assert.equal(state.remoteError, 'timeout')
|
|
await behavior.queueEvidenceUpload.call(state, 'SEAL', 'new.jpg', 'NEW')
|
|
assert.equal(state.evidenceUploading, false); assert.equal(requests[1][3], 'new.jpg'); assert.equal(requests[1][2].sealNo, 'NEW')
|
|
assert.notEqual(requests[1][2].operationId, requests[0][2].operationId)
|
|
})
|
|
|
|
test('failed ordinary photo is removed from the visible and restored draft without deleting saved evidence', async () => {
|
|
const uploadMethod = page.slice(page.indexOf('async queueEvidenceUpload('), page.indexOf('async saveSealNo('))
|
|
const draftMethods = page.slice(page.indexOf('evidenceDraftKey(identity)'), page.indexOf('requireEvidenceCapture() {'))
|
|
const behavior = new Function('isRemoteReady', 'uploadEvidenceFile', 'return ({' + draftMethods + uploadMethod + '})')(
|
|
() => true, async () => { throw new Error('upload failed') }
|
|
)
|
|
const saved = { id: 'saved-photo', fileId: 'saved-file', localPath: 'saved.jpg' }
|
|
const failed = { localPath: 'failed.jpg', uploadStatus: 'PENDING' }
|
|
const seal = { localPath: 'seal.jpg', uploadStatus: 'PENDING' }
|
|
const state = {
|
|
...behavior, evidenceUploading: false, evidenceReadOnly: false,
|
|
evidenceIdentity: { taskId: 'task', warehouseId: 'warehouse' }, evidenceContextId: 1,
|
|
evidenceQueueItems: [], evidenceDrafts: {}, sealNo: 'SEAL-A',
|
|
evidenceFiles: { PHOTO_STEP_1: [saved, failed], SEAL: [seal] },
|
|
validEvidenceEntries: () => [saved], isEvidenceContextActive: () => true,
|
|
remoteFailureMessage: error => error.message, showToast() {}
|
|
}
|
|
state.rememberEvidenceDraft()
|
|
await state.queueEvidenceUpload('PHOTO_STEP_1', 'failed.jpg')
|
|
assert.deepEqual(state.evidenceFiles.PHOTO_STEP_1, [saved])
|
|
assert.equal(state.evidenceUploading, false)
|
|
assert.equal(state.remoteError, 'upload failed')
|
|
state.restoreEvidenceDraft()
|
|
assert.equal(state.evidenceFiles.PHOTO_STEP_1, undefined)
|
|
assert.deepEqual(state.evidenceFiles.SEAL, [seal])
|
|
await state.queueEvidenceUpload('SEAL', 'seal.jpg', 'SEAL-A')
|
|
assert.deepEqual(state.evidenceFiles.SEAL, [seal])
|
|
assert.equal(state.evidenceUploading, false)
|
|
})
|