Merge branch 'release/test' of http://git.buggoglobal.com/html/pda-overseas-uniapp into release/test

This commit is contained in:
2026-09-18 16:48:46 +08:00
20 changed files with 1039 additions and 43 deletions
+100
View File
@@ -162,6 +162,106 @@ test('photo upload refreshes and replays once while retaining the file payload',
])
})
test('H5 blob photo uses authenticated multipart fetch instead of uni.uploadFile', async () => {
const value = prepareSession()
const originalWindow = globalThis.window
const originalLocation = globalThis.location
const originalFetch = globalThis.fetch
let uploadFileCalls = 0
let uploaded
globalThis.window = {}
globalThis.location = { protocol: 'http:' }
globalThis.uni.uploadFile = () => { uploadFileCalls += 1 }
globalThis.fetch = async (url, options) => {
if (String(url).startsWith('blob:')) return new Response(new Blob(['photo'], { type: 'image/png' }))
uploaded = { url, options }
return new Response(JSON.stringify({ code: '200', data: { fileId: 'h5-1' } }), { status: 200 })
}
try {
session.apiBase = '/overseas-api'
assert.deepEqual(await overseasUpload('/api/pda/v1/last-leg/photos/files', 'blob:http://localhost/photo'), { fileId: 'h5-1' })
assert.equal(uploadFileCalls, 0)
assert.equal(uploaded.url, '/overseas-api/pda/v1/last-leg/photos/files')
assert.equal(uploaded.options.headers.Authorization, `Bearer ${value.overseasAccessToken}`)
assert.ok(uploaded.options.body instanceof FormData)
} finally {
globalThis.fetch = originalFetch
if (originalWindow === undefined) delete globalThis.window
else globalThis.window = originalWindow
if (originalLocation === undefined) delete globalThis.location
else globalThis.location = originalLocation
}
})
test('H5 File photo is appended directly without rereading a temporary blob URL', async () => {
const value = prepareSession()
const originalWindow = globalThis.window
const originalLocation = globalThis.location
globalThis.window = {}
globalThis.location = { protocol: 'http:' }
const selected = new Blob(['photo'], { type: 'image/png' })
let calls = 0
globalThis.uni.uploadFile = (options) => {
calls += 1
assert.equal(options.url, '/overseas-api/pda/photo')
assert.equal(options.header.Authorization, `Bearer ${value.overseasAccessToken}`)
assert.equal(options.files[0].file, selected)
assert.equal(options.files[0].name, 'file')
options.success({ statusCode: 200, data: JSON.stringify({ code: '200', data: { fileId: 'direct-file' } }), header: {} })
}
try {
session.apiBase = '/overseas-api'
assert.deepEqual(await overseasUpload('/pda/photo', selected), { fileId: 'direct-file' })
assert.equal(calls, 1)
} finally {
if (originalWindow === undefined) delete globalThis.window
else globalThis.window = originalWindow
if (originalLocation === undefined) delete globalThis.location
else globalThis.location = originalLocation
}
})
test('native file runtime rejects an H5-relative overseas root before upload with a clear configuration error', async () => {
prepareSession()
const originalLocation = globalThis.location
globalThis.location = { protocol: 'file:' }
session.apiBase = '/overseas-api'
let uploadCalls = 0
globalThis.uni.uploadFile = () => { uploadCalls += 1 }
try {
await assert.rejects(overseasUpload('/pda/photo', '_doc/photo.jpg'), (error) => {
assert.equal(error.code, 'PDA_OVERSEAS_API_BASE_REQUIRED')
assert.match(error.message, /VITE_OVERSEAS_API_BASE_URL/)
assert.match(error.message, /HTTP\(S\)/)
return true
})
assert.equal(uploadCalls, 0)
} finally {
if (originalLocation === undefined) delete globalThis.location
else globalThis.location = originalLocation
}
})
test('native file runtime uploads to an absolute overseas root', async () => {
prepareSession()
const originalLocation = globalThis.location
globalThis.location = { protocol: 'file:' }
session.apiBase = 'https://overseas.example.com/api/'
let call
globalThis.uni.uploadFile = (options) => {
call = options
options.success({ statusCode: 200, data: JSON.stringify({ code: '200', data: { fileId: 'native-1' } }), header: {} })
}
try {
assert.deepEqual(await overseasUpload('/api/pda/v1/last-leg/photos/files', '_doc/photo.jpg'), { fileId: 'native-1' })
assert.equal(call.url, 'https://overseas.example.com/api/pda/v1/last-leg/photos/files')
assert.equal(call.filePath, '_doc/photo.jpg')
} finally {
if (originalLocation === undefined) delete globalThis.location
else globalThis.location = originalLocation
}
})
test('authenticated photo download maps the public api path through the H5 overseas proxy', async () => {
const value = prepareSession(); const calls = []
globalThis.uni.downloadFile = (options) => {
+32
View File
@@ -0,0 +1,32 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const source = readFileSync(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
function setup(confirm) {
const calls = []
const uni = { showModal(options) { calls.push(options); options.success({ confirm }) }, redirectTo(options) { calls.push(options) } }
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'uni', body)({}, {}, {}, uni)
const context = { busy: false, stage: 'receive', taskCode: 'PREVIOUS', error: null, candidates: [{ relabelNo: 'RL-ACTIVE', taskStatus: 'PROCESSING', claimedByCurrentOperator: true }] }
context.promptActiveTask = component.methods.promptActiveTask.bind(context)
return { calls, context, open: () => component.methods.openExecution.call(context, { relabelNo: 'RL-OTHER', taskStatus: 'AVAILABLE' }) }
}
test('conflicting task opens modal and confirm resumes owned task without claiming another', async () => {
const { calls, context, open } = setup(true)
await open()
assert.equal(calls[0].confirmText, '前往任务')
assert.equal(calls[0].cancelText, '取消')
assert.match(calls[0].content, /RL-ACTIVE/)
assert.equal(context.taskCode, 'RL-ACTIVE')
assert.equal(calls[1].url, '/pages/relabel/relabel?op=relabelExecute')
assert.equal(context.error, null)
assert.equal(context.busy, false)
})
test('cancel stays on list and preserves existing task selection', async () => {
const { calls, context, open } = setup(false)
await open()
assert.equal(calls.length, 1)
assert.equal(context.taskCode, 'PREVIOUS')
assert.equal(context.busy, false)
})
+14
View File
@@ -0,0 +1,14 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const page = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
test('PDA relabel completion explains pallet and photo blockers instead of silently disabling submit', () => {
assert.match(page, /还不能完成换标/)
assert.match(page, /请先在“换标后组托”中完成本轮已换标箱的拆托合托绑定/)
assert.match(page, /请至少上传 1 张换标完成照片/)
assert.match(page, /:disabled="busy \|\| Boolean\(completionBlockReason\)"/)
assert.match(page, /entry\.action === 'PALLET_BIND'/)
assert.match(page, /detail\.round === round/)
})
+12
View File
@@ -0,0 +1,12 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const page = readFileSync(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
test('PDA 箱唛换标明确展示每箱的新标签和 FBA 目的仓', () => {
assert.match(page, /item\.cartonMark[^\n]+item\.fbaCartonMark/)
assert.match(page, /FBA 目的仓:\{\{ item\.destinationFbaWarehouseCode/)
assert.match(page, /confirmedItem\?\.destinationFbaWarehouseCode \|\| item\.destinationFbaWarehouseCode/)
assert.match(page, /请贴 FBA 箱唛[^`]+FBA 目的仓:\$\{this\.lastCarton\.destinationFbaWarehouseCode/s)
})
+93
View File
@@ -0,0 +1,93 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../src/components/RelabelPalletWork.vue', import.meta.url), 'utf8')
const script = source.split('<script>')[1].split('</script>')[0].replace(/^import .*$/gm, '').replace('export default', 'return')
const component = new Function('ScanTargetInput', script)({})
function context() {
const value = { ...component.data(), $emit() {}, task: { items: [
{ cartonMark: 'A', relabelledPackages: 1, destinationFbaWarehouseCode: 'ONT8' },
{ cartonMark: 'B', relabelledPackages: 1, destinationFbaWarehouseCode: 'JFK8' },
{ cartonMark: 'C', relabelledPackages: 0, destinationFbaWarehouseCode: 'ONT8' },
{ cartonMark: 'OTHER', relabelledPackages: 1, destinationFbaWarehouseCode: 'ONT8' }
] } }
for (const [key, method] of Object.entries(component.methods)) value[key] = method.bind(value)
for (const [key, method] of Object.entries(component.computed)) Object.defineProperty(value, key, { get: method.bind(value) })
value.selectSource({ positionId: '1', palletCode: 'SOURCE', movablePackages: 2, cartonMarks: ['A', 'B', 'C'] })
return value
}
test('quantity batch only includes completed boxes from same inbound and destination', () => {
const value = context()
value.quantity = '1'; value.fillBatch(); assert.deepEqual(value.marks, ['A'])
value.quantity = '2'; value.fillBatch(); assert.match(value.error, /不超过/)
value.quantity = '1.5'; value.fillBatch(); assert.match(value.error, /正整数/)
value.selectedDestination = 'JFK8'; value.quantity = '1'; value.fillBatch(); assert.deepEqual(value.marks, ['B'])
})
test('submission requires exact count and scanned distinct source and target pallets', () => {
const value = context(); value.quantity = '1'; value.fillBatch()
value.source = { palletCode: 'SOURCE' }; value.sourceCode = 'source'
value.target = { palletCode: 'TARGET' }; value.targetCode = 'TARGET'
assert.equal(value.readyToBind, true)
value.toggleMark('A'); assert.equal(value.readyToBind, false)
value.toggleMark('A'); value.sourceCode = 'OTHER'; assert.equal(value.readyToBind, false)
value.sourceCode = 'SOURCE'; value.target = { palletCode: 'SOURCE' }; value.targetCode = 'SOURCE'
assert.equal(value.readyToBind, false)
})
test('quantity disassembly has no extra carton scanner', () => {
assert.doesNotMatch(source, /relabel-move-carton/)
assert.match(source, /actualSource:/)
assert.match(source, /仍归属/)
})
test('latest current-round binding is used for rebind source', () => {
const value = context()
value.task.history = ['OLD', 'NEW'].map(pallet => ({ action: 'PALLET_BIND', detail: JSON.stringify({ round: 'INITIAL', cartonMarks: ['A'], targetPallets: [pallet] }) }))
assert.deepEqual(value.currentBindings, [{ mark: 'A', pallet: 'NEW' }])
value.task.reviewedAt = 'NEW-ROUND'
assert.deepEqual(value.currentBindings, [{ mark: 'A', pallet: 'NEW' }])
value.task.history.push({ action: 'REWORK_ARCHIVE', detail: '{}' })
assert.deepEqual(value.currentBindings, [])
})
test('rescan same bound pallet does not call stock movement', async () => {
const value = context(); value.rebindSelection = { mark: 'A', pallet: 'TARGET' }; value.targetCode = 'target'
let message = ''
value.showBindingMessage = (title, content) => { message = content }
value.run = async () => assert.fail('same pallet must not move stock')
await value.submitRebind()
assert.match(message, /不会重复扣库存/)
})
test('target confirmation submits binding directly, with no second submit button', async () => {
const value = context(); value.quantity = '1'; value.fillBatch()
value.source = { palletCode: 'SOURCE' }; value.sourceCode = 'SOURCE'; value.targetCode = 'TARGET'
let binds = 0
value.resolve = async kind => { assert.equal(kind, 'target'); value.target = { palletCode: 'TARGET' } }
value.bind = async () => { binds++ }
await value.confirmTargetAndBind()
assert.equal(binds, 1)
assert.match(source, /@tap="confirmTargetAndBind"/)
assert.doesNotMatch(source, /@tap="bind"/)
})
test('missing source scan shows modal and never submits a binding', async () => {
const value = context(); value.quantity = '1'; value.fillBatch(); value.targetCode = 'TARGET'
let message = ''
value.showBindingMessage = (title, content) => { message = content }
value.bind = async () => assert.fail('must not bind without source confirmation')
value.resolve = async () => assert.fail('must validate source first')
await value.confirmTargetAndBind()
assert.match(message, /扫描并确认实际来源托盘/)
})
test('target lookup failure cannot submit binding or report success', async () => {
const value = context(); value.quantity = '1'; value.fillBatch()
value.source = { palletCode: 'SOURCE' }; value.sourceCode = 'SOURCE'; value.targetCode = 'TARGET'
value.resolve = async () => { value.error = '目标托盘不可用' }
let title = ''
value.showBindingMessage = heading => { title = heading }
value.bind = async () => assert.fail('must not bind after lookup failure')
await value.confirmTargetAndBind()
assert.equal(title, '绑定失败')
})
+37
View File
@@ -0,0 +1,37 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
const body = source.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', body)({}, {}, {})
function context() {
const value = { current: { reviewedAt: null, history: [], items: [{ itemId: '1', operationType: 'CARTON_AND_SKU', expectedPackages: 1, skippedPackages: 0, exceptionPackages: 0, skuProcessedPackages: 0, cartonProcessedPackages: 1, skuItems: [{ skuCode: 'OLD', newSkuCode: 'NEW', quantity: 3 }, { skuCode: 'OTHER', newSkuCode: 'NEW2', quantity: 2 }] }] }, taskTab: 'sku' }
for (const name of ['skuDone', 'skuDetails', 'remaining']) value[name] = component.methods[name].bind(value)
for (const name of ['visibleItems', 'pendingSkuRows']) Object.defineProperty(value, name, { get: component.computed[name].bind(value) })
return value
}
test('each persisted SKU hides independently; mixed carton progress is separate', () => {
const value = context(); assert.equal(value.pendingSkuRows.length, 2)
value.current.history.push({ action: 'SKU_SCAN', detail: JSON.stringify({ round: 'INITIAL', itemId: '1', skuIndex: 0 }) })
assert.equal(value.pendingSkuRows.length, 1); assert.equal(value.pendingSkuRows[0].sku.skuCode, 'OTHER')
value.taskTab = 'carton'; assert.equal(value.visibleItems.length, 0)
value.taskTab = 'sku'; value.current.reviewedAt = '2026-09-15T18:00:00'; assert.equal(value.pendingSkuRows.length, 2)
value.current.items[0].skuProcessedPackages = 1; assert.equal(value.visibleItems.length, 0)
})
test('normal SKU input is always visible in SKU tab and never requests the new label', () => {
assert.match(source, /name="quickSku"/)
assert.doesNotMatch(source, /placeholder="扫描或输入新 SKU 标签号"/)
assert.match(source, /matches.length > 1/)
assert.match(source, /scanRelabelSku/)
})
test('pending list removes one confirmed row at a time without persistent empty or last-scan banners', () => {
const value = context()
assert.equal(value.pendingSkuRows.length, 2)
for (const index of [0, 1]) {
value.current.history.push({ action: 'SKU_SCAN', detail: JSON.stringify({ round: 'INITIAL', itemId: '1', skuIndex: index }) })
assert.equal(value.pendingSkuRows.length, 1 - index)
}
const template = source.split('<script>')[0]
assert.doesNotMatch(template, /待处理明细已清空|v-if="lastSku"|v-if="lastCarton"/)
assert.match(source, /uni.showModal/)
})
+117
View File
@@ -0,0 +1,117 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
test('warehouse transfer PDA API exposes candidates and palletize operations', async () => {
const source = await import('../src/api/warehouse-transfers.js')
assert.equal(typeof source.listWarehouseTransferCandidates, 'function')
assert.equal(typeof source.getWarehouseTransferDetail, 'function')
assert.equal(typeof source.palletizeWarehouseTransfer, 'function')
assert.equal(typeof source.saveWarehouseTransferPalletDraft, 'function')
assert.equal(typeof source.generateWarehouseTransferPallet, 'function')
assert.equal(typeof source.bindWarehouseTransferPallet, 'function')
assert.equal(typeof source.deleteWarehouseTransferPallet, 'function')
assert.equal(typeof source.uploadWarehouseTransferMeasurementPhoto, 'function')
assert.equal(typeof source.listWarehouseTransferMeasurementPhotos, 'function')
assert.equal(typeof source.downloadWarehouseTransferMeasurementPhoto, 'function')
assert.equal(typeof source.downloadWarehouseTransferPalletLabel, 'function')
})
test('warehouse transfer workbench entry is registered', async () => {
const { MODULES } = await import('../src/data/overseas-workbench.js')
const operation = MODULES.flatMap((m) => m.operations).find((item) => item.key === 'warehouseTransfer')
assert.equal(operation?.available, true)
assert.equal(operation?.route, '/pages/warehouse-transfer/warehouse-transfer')
})
test('warehouse transfer page loads detail without exposing internal identifiers', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /getWarehouseTransferDetail\(item\.transferNo\)/)
assert.doesNotMatch(page, /转运明细 ID|manualItems/)
})
test('warehouse transfer shows forecast and actual variance without blocking short or over receipt', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
for (const label of ['客户预报', '当前实收', '短收差异', '超收差异']) assert.match(page, new RegExp(label))
assert.match(page, /cartonDifference/)
assert.doesNotMatch(page, /全部托盘箱数必须等于订单计划箱数/)
assert.doesNotMatch(page, /本托 SKU 箱数|itemCounts|transferItemId/)
})
test('warehouse transfer retries reuse the pending idempotency key', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /if \(!this\.pendingKey\) this\.pendingKey = `pda-transfer-/)
assert.match(page, /idempotencyKey: this\.pendingKey/)
assert.equal((page.match(/pendingKey = `pda-transfer-/g) || []).length, 1)
})
test('warehouse transfer measurement inputs retain explicit placeholders', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
for (const placeholder of ['长度', '宽度', '高度', '毛重']) assert.match(page, new RegExp(`placeholder="${placeholder}"`))
})
test('warehouse transfer measurements are grouped and summarized per pallet', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /v-for="\(pallet, index\) in pallets"/)
for (const label of ['本托实测', '当前托重量', '当前托箱数', '自动体积']) assert.match(page, new RegExp(label))
assert.match(page, /palletVolume\(pallet\)/)
assert.match(page, /rawPalletVolume\(pallet\)/)
})
test('warehouse transfer requires a camera photo and submits its file id with palletization', async () => {
const [page, api] = await Promise.all([
readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8'),
readFile(new URL('../src/api/warehouse-transfers.js', import.meta.url), 'utf8')
])
assert.match(page, /sourceType: \['camera', 'album'\]/)
assert.match(page, /请至少拍摄 1 张测量现场照片/)
assert.match(page, /photoFileIds: \[\.\.\.this\.photoFileIds\]/)
assert.match(api, /measurement-photos\/files/)
assert.match(page, /tempFilePaths \|\| \[\]/)
assert.match(page, /未获取到可上传的照片路径,请重新拍摄/)
assert.match(page, /所选转运单已切换,请重新拍摄/)
})
test('warehouse transfer restores uploaded photos and exposes honest pallet label printing', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /listWarehouseTransferMeasurementPhotos/)
assert.match(page, /downloadWarehouseTransferMeasurementPhoto/)
assert.match(page, /completedPallets = \(result\?\.pallets \|\| \[\]\)/)
assert.match(page, /downloadWarehouseTransferPalletLabel/)
assert.match(page, /pallet\.labelDownloadPath/)
assert.match(page, /pallet\.fbaWarehouseCode \|\| completedDestination \|\| '目的仓待定'/)
assert.match(page, /目的仓待定/)
assert.match(page, /uni\.openDocument\(\{ filePath, fileType: 'pdf', showMenu: true/)
assert.doesNotMatch(page, /批量打印|printAllPallets/)
})
test('warehouse transfer always requests newly generated pallets', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.doesNotMatch(page, /托盘码\s*<small>选填|扫描或输入当前托盘码/)
assert.doesNotMatch(page, /palletCode: String\(p\.palletCode/)
assert.match(page, /completedPallets = \(result\?\.pallets \|\| \[\]\)/)
})
test('warehouse transfer pallet cards prepare generated and server-bound states', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /生成托盘号/)
assert.match(page, /pallet\.generated && !pallet\.bound/)
assert.match(page, /ScanTargetInput/)
assert.match(page, /startScanBroadcast/)
assert.match(page, /handlePalletScan/)
assert.match(page, /扫描或手输新托盘标签号/)
assert.match(page, /已绑定实物托盘/)
assert.match(page, /!pallet\.generated.*removePallet/)
assert.match(page, /:disabled="pallet\.generated"/)
assert.match(page, /collapsed: false/)
assert.match(page, /pallet\.collapsed = !pallet\.collapsed/)
assert.match(page, /v-show="!pallet\.collapsed"/)
assert.match(page, /this\.selected\.pallets\?\.length \? this\.selected\.pallets\.map\(restoredPallet\)/)
assert.doesNotMatch(page, /if \(!this\.completedTransferNo \|\| this\.openingLabel\) return/)
assert.match(page, /saveWarehouseTransferPalletDraft/)
assert.match(page, /generateWarehouseTransferPallet/)
assert.match(page, /bindWarehouseTransferPallet/)
assert.match(page, /deleteWarehouseTransferPallet/)
assert.match(page, /请先生成并扫描确认绑定全部托盘/)
assert.match(page, /const payload = \{ idempotencyKey: this\.pendingKey, photoFileIds: \[\.\.\.this\.photoFileIds\] \}/)
})
+26 -4
View File
@@ -80,7 +80,8 @@ test('relabel API covers claim, unit outcomes, completion and review with encode
test('carton relabel uses one scan input and shows the confirmed FBA mapping after saving', async () => {
const page = await read('../src/pages/relabel/relabel.vue')
assert.equal((page.match(/name="quickCarton"/g) || []).length, 1)
assert.match(page, /v-if="taskTab === 'sku' && selectedItem && remaining\(selectedItem\) > 0"/)
assert.match(page, /name="quickSku"/)
assert.match(page, /v-if="taskTab === 'sku' && selectedItem && action !== 'RELABEL' && remaining\(selectedItem\) > 0"/)
assert.match(page, /selectItem\(item\) \{ if \(this\.taskTab === 'carton'\) \{ this\.activeTarget = 'quick-carton'; return \}/)
const start = page.indexOf('async submitCarton(raw)')
const scan = page.slice(start, page.indexOf('removePhoto(index)', start))
@@ -90,6 +91,17 @@ test('carton relabel uses one scan input and shows the confirmed FBA mapping aft
assert.match(scan, /confirmedItem\?\.fbaCartonMark/)
})
test('mixed relabel keeps SKU work open after carton work and only carton tab mounts pallet work', async () => {
const page = await read('../src/pages/relabel/relabel.vue')
const body = page.match(/remaining\(item\) \{([^\n]+)\},/)[1]
const remaining = new Function('item', body)
const mixed = { operationType: 'CARTON_AND_SKU', expectedPackages: 1, relabelledPackages: 1,
cartonProcessedPackages: 1, skuProcessedPackages: 0, skippedPackages: 0, exceptionPackages: 0 }
assert.equal(remaining.call({ taskTab: 'sku' }, mixed), 1)
assert.equal(remaining.call({ taskTab: 'carton' }, mixed), 0)
assert.match(page, /<RelabelPalletWork v-if="taskTab === 'carton'"/)
})
test('relabel list opens a row directly in execution and completion returns to the list', async () => {
const page = await read('../src/pages/relabel/relabel.vue')
assert.doesNotMatch(page, /name="relabelTask"/)
@@ -105,13 +117,23 @@ test('relabel list opens a row directly in execution and completion returns to t
test('relabel review is a read-only list and completed execution details stay hidden', async () => {
const page = await read('../src/pages/relabel/relabel.vue')
assert.match(page, /showCurrent\(\) \{ if \(!this\.current \|\| this\.stage === 'review'\) return false; if \(this\.stage === 'execute'\) return this\.current\.taskStatus === 'PROCESSING' && this\.current\.claimedByCurrentOperator/)
const body = page.match(/showCurrent\(\) \{([^\n]+)\},/)[1]
const visible = new Function(body)
const context = { stage: 'execute', current: { relabelNo: 'RL-1', taskStatus: 'PROCESSING' }, candidates: [{ relabelNo: 'RL-1', taskStatus: 'PROCESSING', claimedByCurrentOperator: true }] }
assert.equal(visible.call(context), true, 'detail without ownership field must use server candidate ownership')
assert.equal(visible.call({ ...context, candidates: [] }), false)
assert.equal(visible.call({ ...context, stage: 'review' }), false)
assert.equal(visible.call({ ...context, current: { ...context.current, taskStatus: 'WAIT_REVIEW' } }), false)
assert.match(page, /stageSubtitle\(\) \{ return this\.stage === 'review' \? '仅展示当前仓库已完成现场换标并等待复核的单据'/)
const reviewList = page.slice(page.indexOf('<view v-else class="task-list">'), page.indexOf('<view v-if="showCurrent"'))
assert.doesNotMatch(reviewList, /@tap|详情/)
assert.match(page, /<button v-if="stage !== 'review'" class="link-button"[^>]*@tap="loadCandidates"/)
assert.match(page, /<button v-if="stage !== 'review'" class="link-button"[^>]*@tap="bootstrap"/)
assert.match(page, /if \(this\.stage === 'execute'\) this\.startScanner\(\)/)
assert.match(page, /if \(this\.stage === 'execute' && this\.taskCode\) await this\.openTask/)
const bootstrapBody = page.match(/async bootstrap\(\) \{([^\n]+)\},/)[1]
const bootstrap = new (Object.getPrototypeOf(async function () {}).constructor)(bootstrapBody)
const opened = []
await bootstrap.call({ ...context, taskCode: 'STALE', loadCandidates: async () => {}, openTask: async (no) => opened.push(no) })
assert.deepEqual(opened, ['RL-1'])
})
test('warehouse drafts are isolated by operator and warehouse and retain retry keys until success', () => {
+2
View File
@@ -7,6 +7,7 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
const enabled = operations.filter((operation) => operation.available)
assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [
{ module: 'inbound', name: '拆柜收货' },
{ module: 'inbound', name: '仓储转运测量' },
{ module: 'inbound', name: '退货入库' },
{ module: 'inbound', name: '退货合箱' },
{ module: 'outbound', name: '排车拣货' },
@@ -25,6 +26,7 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
assert.equal(operations.find((item) => item.name === '出库作业')?.route, '/pages/outbound/outbound')
assert.equal(operations.find((item) => item.name === '退货入库')?.route, '/pages/return-receipt/return-receipt')
assert.equal(operations.find((item) => item.name === '退货合箱')?.route, '/pages/return-packing/return-packing')
assert.equal(operations.find((item) => item.name === '仓储转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer')
assert.equal(operations.some((item) => item.name === '库存盘点'), false)