Merge branch 'release/test' of http://git.buggoglobal.com/html/pda-overseas-uniapp into release/test
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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/)
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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, '绑定失败')
|
||||
})
|
||||
@@ -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/)
|
||||
})
|
||||
@@ -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\] \}/)
|
||||
})
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user