fix(pda): 修复海外作业兼容性并完善扫码与装车收尾
This commit is contained in:
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fulfillment, restoreFulfillmentDraft, updateFulfillmentDraft, resetFulfillmentSession } from '../src/stores/fulfillment.js'
|
||||
import { requireDispatchVersion, dispatchVersionAtLeast, normalizeDispatchTask } from '../src/api/fulfillment.js'
|
||||
import { formatDispatchReviewTime } from '../src/utils/dispatch-time.js'
|
||||
const source = (await readFile(new URL('../src/pages/dispatch/dispatch.vue', import.meta.url), 'utf8')).match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
const item = (overrides = {}) => ({ dispatchItemId: 'item-1', sourceKey: 'source-1', sourceBusinessType: 'RELABEL_ORDER', sourceBusinessNo: 'RL-1', customerId: 'customer-1', customerName: '客户甲', palletNo: 'PALLET-1', scheduledPackages: 2, pickedPackages: 0, positions: [{ positionId: 'position-1', availablePackages: 2, allocatedPackages: 0, pickedPackages: 0 }], returnCartons: [], ...overrides })
|
||||
test('native string quantities normalize without changing Long identity or version', () => {
|
||||
@@ -25,7 +26,7 @@ const task = (overrides = {}) => normalizeDispatchTask({ taskNo: 'TASK-1', dispa
|
||||
function storage() { const values = new Map(); globalThis.uni = { setStorageSync: (key, value) => values.set(key, structuredClone(value)), getStorageSync: key => structuredClone(values.get(key)), removeStorageSync: key => values.delete(key), showModal: async () => ({ confirm: true }) }; resetFulfillmentSession(); restoreFulfillmentDraft('operator:warehouse'); return values }
|
||||
let lastCommand
|
||||
function page(dependencies = {}) {
|
||||
const deps = { PortalHeader: {}, ScanTargetInput: {}, DispatchHandoverStatus: {}, DispatchLoadingDocuments: {}, session: { currentUser: {} }, fulfillment, requireDispatchVersion, dispatchVersionAtLeast, updateFulfillmentDraft, playSuccessSound() {}, playErrorSound() {}, toDispatchError: error => ({ message: error.message }), ...dependencies }
|
||||
const deps = { PortalHeader: {}, ScanTargetInput: {}, DispatchHandoverStatus: {}, DispatchLoadingDocuments: {}, session: { currentUser: {} }, fulfillment, requireDispatchVersion, dispatchVersionAtLeast, updateFulfillmentDraft, formatDispatchReviewTime, playSuccessSound() {}, playErrorSound() {}, toDispatchError: error => ({ message: error.message }), ...dependencies }
|
||||
for (const [api, action] of [['pickDispatchItem', 'PICK'], ['completeDispatchLoading', 'LOADING_COMPLETE'], ['reviewDispatchLoading', 'LOADING_REVIEW']]) {
|
||||
const original = deps[api]; if (!original) continue
|
||||
deps[api] = (...args) => { lastCommand = { dispatchNo: args[0], action, body: args.at(-1) }; return original(...args) }
|
||||
@@ -296,7 +297,10 @@ test('missing appointment blocks approval while preserving rejection to customer
|
||||
await state.submitReview('REJECTED'); assert.equal(sent.length, 1); assert.equal(sent[0][1].decision, 'REJECTED')
|
||||
})
|
||||
|
||||
for (const [instant, expected] of [
|
||||
const reviewTimeExamples = [
|
||||
['2006-03-20T15:00:00Z', '美东 2006-03-20 10:00\n美西 2006-03-20 07:00'],
|
||||
['2040-07-01T15:00:00Z', '美东 2040-07-01 11:00\n美西 2040-07-01 08:00'],
|
||||
['2026-01-01T05:00:00Z', '美东 2026-01-01 00:00\n美西 2025-12-31 21:00'],
|
||||
['2026-11-06T00:00:00+08:00', '美东 2026-11-05 11:00\n美西 2026-11-05 08:00'],
|
||||
['2026-11-05T16:00:00Z', '美东 2026-11-05 11:00\n美西 2026-11-05 08:00'],
|
||||
['2026-07-01T15:00:00Z', '美东 2026-07-01 11:00\n美西 2026-07-01 08:00'],
|
||||
@@ -306,10 +310,64 @@ for (const [instant, expected] of [
|
||||
['2026-11-01T05:30:00Z', '美东 2026-11-01 01:30\n美西 2026-10-31 22:30'],
|
||||
['2026-11-01T07:30:00Z', '美东 2026-11-01 02:30\n美西 2026-11-01 00:30'],
|
||||
['2026-11-01T10:30:00Z', '美东 2026-11-01 05:30\n美西 2026-11-01 02:30'],
|
||||
]) test(`review times use eastern and pacific daylight-saving rules for ${instant}`, () => {
|
||||
]
|
||||
for (const [instant, expected] of reviewTimeExamples) test(`review times use eastern and pacific daylight-saving rules for ${instant}`, () => {
|
||||
assert.equal(page().reviewTime(instant), expected)
|
||||
})
|
||||
|
||||
test('review times render without Intl using Android timezone offsets across daylight-saving changes', t => {
|
||||
const intlDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Intl')
|
||||
const plusDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'plus')
|
||||
const DateTimeFormat = Intl.DateTimeFormat
|
||||
t.after(() => {
|
||||
Object.defineProperty(globalThis, 'Intl', intlDescriptor)
|
||||
if (plusDescriptor) Object.defineProperty(globalThis, 'plus', plusDescriptor)
|
||||
else delete globalThis.plus
|
||||
})
|
||||
delete globalThis.Intl
|
||||
const calls = []
|
||||
globalThis.plus = { android: { invoke(target, method, argument) {
|
||||
calls.push({ target, method, argument })
|
||||
if (method === 'getTimeZone') {
|
||||
assert.equal(target, 'java.util.TimeZone')
|
||||
return { timeZone: argument }
|
||||
}
|
||||
assert.equal(method, 'getOffset')
|
||||
const parts = new DateTimeFormat('en-GB', { timeZone: target.timeZone, timeZoneName: 'longOffset' }).formatToParts(argument)
|
||||
const offset = parts.find(part => part.type === 'timeZoneName').value.match(/^GMT([+-])(\d{2}):(\d{2})$/)
|
||||
return (offset[1] === '-' ? -1 : 1) * (Number(offset[2]) * 60 + Number(offset[3])) * 60000
|
||||
} } }
|
||||
const state = page()
|
||||
state.current = reviewTask()
|
||||
assert.equal(state.canReview, true)
|
||||
for (const [instant, expected] of reviewTimeExamples) assert.equal(state.reviewTime(instant), expected)
|
||||
assert.equal(calls.filter(call => call.method === 'getOffset').length, reviewTimeExamples.length * 2)
|
||||
assert.equal(calls.find(call => call.method === 'getOffset').argument, Date.parse(reviewTimeExamples[0][0]))
|
||||
})
|
||||
|
||||
test('review times show explicit UTC when Intl and the Android timezone bridge are unavailable', t => {
|
||||
const intlDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Intl')
|
||||
const plusDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'plus')
|
||||
t.after(() => {
|
||||
Object.defineProperty(globalThis, 'Intl', intlDescriptor)
|
||||
if (plusDescriptor) Object.defineProperty(globalThis, 'plus', plusDescriptor)
|
||||
else delete globalThis.plus
|
||||
})
|
||||
delete globalThis.Intl
|
||||
delete globalThis.plus
|
||||
const state = page()
|
||||
assert.equal(state.reviewTime('2026-11-06T00:00:00+08:00'), 'UTC 2026-11-05 16:00')
|
||||
globalThis.plus = { android: { invoke() { return undefined } } }
|
||||
assert.equal(state.reviewTime('2026-07-01T15:00:00Z'), 'UTC 2026-07-01 15:00')
|
||||
globalThis.plus.android.invoke = () => { throw new Error('Native bridge unavailable') }
|
||||
assert.equal(state.reviewTime('2026-07-01T15:00:00Z'), 'UTC 2026-07-01 15:00')
|
||||
delete globalThis.plus
|
||||
globalThis.Intl = { DateTimeFormat: function () { return {} } }
|
||||
assert.equal(state.reviewTime('2026-07-01T15:00:00Z'), 'UTC 2026-07-01 15:00')
|
||||
assert.equal(state.reviewTime(null), '未填写')
|
||||
assert.equal(state.reviewTime('2026-11-06T00:00:00'), '时间无效')
|
||||
})
|
||||
|
||||
test('review times preserve missing values and reject invalid or timezone-free timestamps', () => {
|
||||
const state = page()
|
||||
for (const missing of [null, undefined, '']) assert.equal(state.reviewTime(missing), '未填写')
|
||||
@@ -381,6 +439,44 @@ test('photo tab preserves the selected pallet and quantity and ignores picking b
|
||||
state.selectTab('picking'); assert.equal(state.selectedItem.palletNo, 'PALLET-1'); assert.equal(state.packages, 1)
|
||||
})
|
||||
|
||||
test('saving a complete set of loading photos returns to the refreshed task list', async () => {
|
||||
storage(); let reads = 0
|
||||
const rows = [task({ dispatchNo: 'PC/NEXT' })]
|
||||
const state = page({ listDispatchTasks: async () => { reads++; return rows } })
|
||||
state.selectTab('photos')
|
||||
await state.onLoadingDocumentsSaved({ dispatchNo: 'PC/1', complete: true })
|
||||
assert.equal(state.current, null)
|
||||
assert.equal(state.activeTab, 'picking')
|
||||
assert.equal(fulfillment.dispatch.currentNo, '')
|
||||
assert.deepEqual(state.tasks, rows)
|
||||
assert.equal(reads, 1)
|
||||
})
|
||||
|
||||
test('partial photo saves stay on photos and callbacks from another task are ignored', async () => {
|
||||
storage(); let reads = 0
|
||||
const state = page({ getDispatchTask: async no => { reads++; return task({ dispatchNo: no, recordVersion: 2 }) } })
|
||||
state.selectTab('photos')
|
||||
await state.onLoadingDocumentsSaved({ dispatchNo: 'PC/OLD', complete: true })
|
||||
assert.equal(reads, 0)
|
||||
assert.equal(state.current.dispatchNo, 'PC/1')
|
||||
await state.onLoadingDocumentsSaved({ dispatchNo: 'PC/1', complete: false })
|
||||
assert.equal(reads, 1)
|
||||
assert.equal(state.activeTab, 'photos')
|
||||
assert.equal(state.current.dispatchNo, 'PC/1')
|
||||
})
|
||||
|
||||
test('a failed task-list refresh after completing photos stays on the list with a retry', async () => {
|
||||
storage(); let reads = 0
|
||||
const state = page({ listDispatchTasks: async () => { if (++reads === 1) throw new Error('列表刷新失败'); return [] } })
|
||||
await state.onLoadingDocumentsSaved({ dispatchNo: 'PC/1', complete: true })
|
||||
assert.equal(state.current, null)
|
||||
assert.equal(state.error.message, '列表刷新失败')
|
||||
assert.equal(state.loading, false)
|
||||
await state.retry()
|
||||
assert.equal(state.error, null)
|
||||
assert.equal(reads, 2)
|
||||
})
|
||||
|
||||
test('same-task photo refresh stays on photos while a new task starts on picking', async () => {
|
||||
storage(); const state = page({ getDispatchTask: async no => task({ dispatchNo: no, recordVersion: 2 }) })
|
||||
state.selectTab('photos'); await state.openTask('PC/1'); assert.equal(state.activeTab, 'photos')
|
||||
|
||||
Reference in New Issue
Block a user