2026-10-14 13:53:24 +08:00
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import { readFile } from 'node:fs/promises'
|
2026-10-16 11:43:15 +08:00
|
|
|
|
import { createSSRApp } from 'vue'
|
|
|
|
|
|
import { renderToString } from 'vue/server-renderer'
|
2026-10-14 13:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
function setup() {
|
|
|
|
|
|
const calls = []
|
2026-10-16 11:43:15 +08:00
|
|
|
|
const component = new Function('ScanTargetInput', 'measureRelabelPallet', 'warehouseIdempotencyKey', 'releaseWarehouseKey', 'bindRelabelPallet', script)(
|
2026-10-14 13:53:24 +08:00
|
|
|
|
{}, async payload => { calls.push(payload); return [{ palletCode: payload.palletCode, volumeCbm: 0.25, grossWeightKg: payload.grossWeightKg }] },
|
2026-10-16 11:43:15 +08:00
|
|
|
|
() => ({ value: 'stable-key', signature: 'signature' }), () => {}, async payload => calls.push(payload))
|
2026-10-14 13:53:24 +08:00
|
|
|
|
const value = { ...component.data(), task: { relabelNo: 'RL-1' }, $emit() {} }
|
|
|
|
|
|
for (const [key, method] of Object.entries(component.methods)) value[key] = method.bind(value)
|
|
|
|
|
|
return { component, value, calls }
|
|
|
|
|
|
}
|
|
|
|
|
|
test('pallet measurement submits actual dimensions, weight and current stock version and refreshes results', async () => {
|
|
|
|
|
|
const { value, calls } = setup()
|
|
|
|
|
|
await value.saveMeasurement({ palletCode: 'NEW', stockVersion: 'position:version:count', lengthCm: '100', widthCm: '50', heightCm: '50', grossWeightKg: '40' })
|
|
|
|
|
|
assert.deepEqual(calls, [{ relabelNo: 'RL-1', palletCode: 'NEW', stockVersion: 'position:version:count', lengthCm: 100, widthCm: 50, heightCm: 50, grossWeightKg: 40, idempotencyKey: 'stable-key' }])
|
|
|
|
|
|
assert.equal(value.measurements[0].volumeCbm, 0.25)
|
|
|
|
|
|
assert.equal(value.busy, false)
|
|
|
|
|
|
await value.saveMeasurement({ palletCode: 'NEW', lengthCm: 0, widthCm: 50, heightCm: 50, grossWeightKg: 40 })
|
|
|
|
|
|
assert.equal(calls.length, 1)
|
|
|
|
|
|
assert.match(value.error, /有效/)
|
|
|
|
|
|
})
|
|
|
|
|
|
test('only whole physical pallet quantities offer original pallet reuse', () => {
|
|
|
|
|
|
const { component } = setup()
|
|
|
|
|
|
const canRetain = component.computed.canRetainOriginal
|
|
|
|
|
|
const ctx = { source: { totalPackages: 10 }, selectedSource: {}, plannedPackages: 10, isReturnBulk: false, unboundSourceMode: false }
|
|
|
|
|
|
assert.equal(canRetain.call(ctx), true)
|
|
|
|
|
|
ctx.plannedPackages = 4
|
|
|
|
|
|
assert.equal(canRetain.call(ctx), false)
|
|
|
|
|
|
ctx.plannedPackages = 10;ctx.isReturnBulk = true
|
|
|
|
|
|
assert.equal(canRetain.call(ctx), false)
|
|
|
|
|
|
})
|
2026-10-16 11:43:15 +08:00
|
|
|
|
|
|
|
|
|
|
async function renderMeasurements(measurements) {
|
|
|
|
|
|
const { component } = setup()
|
|
|
|
|
|
const template = source.slice(source.indexOf('<template>') + 10, source.indexOf('<script>')).trim().replace(/<\/template>$/, '')
|
|
|
|
|
|
return renderToString(createSSRApp({ ...component, template,
|
|
|
|
|
|
components: { ScanTargetInput: { template: '<view />' } },
|
|
|
|
|
|
props: undefined,
|
|
|
|
|
|
data() { return { ...component.data(), task: { relabelNo: 'RL-1', items: [], history: [] }, measurements } }
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
test('unchanged source renders real grouped box specs and totals without zero pallet dimension inputs', async () => {
|
|
|
|
|
|
const html = await renderMeasurements([
|
|
|
|
|
|
{ palletCode: 'FIRST', packages: 2, inherited: true, lengthCm: null, widthCm: null, heightCm: null,
|
|
|
|
|
|
grossWeightKg: 5, volumeCbm: 0.3, sourceCartonSpecs: [{ lengthCm: 60, widthCm: 50, heightCm: 50, grossWeightKg: 2.5, cartons: 2 }] },
|
|
|
|
|
|
{ palletCode: 'SECOND', packages: 3, inherited: true, lengthCm: null, widthCm: null, heightCm: null,
|
|
|
|
|
|
grossWeightKg: 9.7, volumeCbm: 0.31, sourceCartonSpecs: [
|
|
|
|
|
|
{ lengthCm: 60, widthCm: 50, heightCm: 50, grossWeightKg: 2.5, cartons: 1 },
|
|
|
|
|
|
{ lengthCm: 50, widthCm: 40, heightCm: 40, grossWeightKg: 3.6, cartons: 2 }] }
|
|
|
|
|
|
])
|
|
|
|
|
|
assert.match(html, /60 × 50 × 50 cm · 2.5 kg\/箱 · 2 箱/)
|
|
|
|
|
|
assert.match(html, /50 × 40 × 40 cm · 3.6 kg\/箱 · 2 箱/)
|
|
|
|
|
|
assert.match(html, /来源汇总:0.31 m³ · 9.7 kg/)
|
2026-10-16 18:26:06 +08:00
|
|
|
|
assert.doesNotMatch(html, /未测整托长宽高/)
|
2026-10-16 11:43:15 +08:00
|
|
|
|
assert.equal((html.match(/class="measurement-inputs"/g) || []).length, 0)
|
|
|
|
|
|
assert.doesNotMatch(html, /确认本托实测/)
|
|
|
|
|
|
})
|
|
|
|
|
|
test('changed pallets and explicit packaging changes still render final pallet measurement inputs', async () => {
|
|
|
|
|
|
for (const pallet of [{ inherited: false }, { inherited: true, remeasuring: true }]) {
|
|
|
|
|
|
const html = await renderMeasurements([{ ...pallet, palletCode: 'P', packages: 1 }])
|
|
|
|
|
|
assert.match(html, /class="measurement-inputs"/)
|
|
|
|
|
|
assert.match(html, /确认本托实测/)
|
|
|
|
|
|
assert.match(html, /长 cm/)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
test('box aggregated pallets render current totals and no manual pallet measurement even after moving', async () => {
|
|
|
|
|
|
const html = await renderMeasurements([{ palletCode: 'NEW', packages: 1, inherited: true, cartonAggregated: true,
|
|
|
|
|
|
grossWeightKg: 3.6, volumeCbm: 0.08, sourceCartonSpecs: [{lengthCm:50,widthCm:40,heightCm:40,grossWeightKg:3.6,cartons:1}] }])
|
2026-10-16 18:26:06 +08:00
|
|
|
|
assert.doesNotMatch(html, /移出扣减、移入增加/)
|
2026-10-16 11:43:15 +08:00
|
|
|
|
assert.match(html, /0.08 m³ · 3.6 kg/)
|
|
|
|
|
|
assert.doesNotMatch(html, /确认本托实测|重新测量|measurement-inputs/)
|
|
|
|
|
|
})
|
2026-10-16 18:26:06 +08:00
|
|
|
|
const mixedBoxes = [
|
|
|
|
|
|
{ cartonMark: 'OTHER', lengthCm: 60, widthCm: 50, heightCm: 50, grossWeightKg: 2.5 },
|
|
|
|
|
|
{ cartonMark: 'SMALL1', customerCartonMark: 'CUSTOMER', lengthCm: 50, widthCm: 40, heightCm: 40, grossWeightKg: 3.6 }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
test('mixed specs automatically bind the submitted carton without another picker', async () => {
|
2026-10-16 11:43:15 +08:00
|
|
|
|
const { value, calls } = setup()
|
2026-10-16 18:26:06 +08:00
|
|
|
|
Object.assign(value, { readyToBind: true, requiresMeasuredBoxMapping: true, marks: ['CUSTOMER'], measuredBoxChoices: mixedBoxes,
|
|
|
|
|
|
source: { palletCode: 'OLD', palletVersion: 1 }, target: { palletCode: 'NEW', palletVersion: 1 },
|
|
|
|
|
|
selectedSource: { positionId: '1', positionVersion: 1 }, quantity: '1', loadSources: async () => {}, showBindingMessage() {} })
|
2026-10-16 11:43:15 +08:00
|
|
|
|
await value.bind()
|
2026-10-16 18:26:06 +08:00
|
|
|
|
assert.deepEqual(calls[0].cartonMarks, ['CUSTOMER'])
|
|
|
|
|
|
assert.deepEqual(calls[0].measuredCartons, [{ cartonMark: 'CUSTOMER', sourceCartonMark: 'SMALL1' }])
|
|
|
|
|
|
assert.doesNotMatch(source, /<picker|chooseMeasuredBox|点击选择实测箱/)
|
2026-10-16 11:43:15 +08:00
|
|
|
|
})
|
2026-10-16 18:26:06 +08:00
|
|
|
|
|
|
|
|
|
|
test('warehouse carton marks also match automatically and follow the current batch', () => {
|
2026-10-16 11:43:15 +08:00
|
|
|
|
const { value } = setup()
|
2026-10-16 18:26:06 +08:00
|
|
|
|
Object.assign(value, { requiresMeasuredBoxMapping: true, measuredBoxChoices: mixedBoxes, marks: ['SMALL1'] })
|
|
|
|
|
|
assert.deepEqual(value.measuredCartonBindings(), [{ cartonMark: 'SMALL1', sourceCartonMark: 'SMALL1' }])
|
|
|
|
|
|
value.marks = ['OTHER']
|
|
|
|
|
|
assert.deepEqual(value.measuredCartonBindings(), [{ cartonMark: 'OTHER', sourceCartonMark: 'OTHER' }])
|
2026-10-16 11:43:15 +08:00
|
|
|
|
})
|
2026-10-16 18:26:06 +08:00
|
|
|
|
|
|
|
|
|
|
test('unmatched, ambiguous or duplicate physical boxes cannot bind another carton', async () => {
|
|
|
|
|
|
for (const [marks, boxes] of [
|
|
|
|
|
|
[['MISSING'], mixedBoxes],
|
|
|
|
|
|
[['CUSTOMER'], [...mixedBoxes, { ...mixedBoxes[0], customerCartonMark: 'CUSTOMER' }]],
|
|
|
|
|
|
[['CUSTOMER', 'SMALL1'], mixedBoxes]
|
|
|
|
|
|
]) {
|
|
|
|
|
|
const { value, calls } = setup()
|
|
|
|
|
|
Object.assign(value, { readyToBind: true, requiresMeasuredBoxMapping: true, measuredBoxChoices: boxes, marks })
|
|
|
|
|
|
await value.bind()
|
|
|
|
|
|
assert.equal(calls.length, 0)
|
|
|
|
|
|
assert.match(value.error, /不匹配/)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('entering split quantity prepares the batch and clears invalid quantities', () => {
|
2026-10-16 11:43:15 +08:00
|
|
|
|
const { value } = setup()
|
2026-10-16 18:26:06 +08:00
|
|
|
|
Object.assign(value, { measuredBoxChoices: [{ cartonMark: 'A' }], eligibleItems: [{ cartonMark: 'X' }],
|
|
|
|
|
|
selectedSource: { movablePackages: 1 }, fillBatch() { this.marks = ['X'] } })
|
|
|
|
|
|
value.prepareMeasuredBatch({ detail: { value: '1' } })
|
|
|
|
|
|
assert.deepEqual(value.marks, ['X'])
|
|
|
|
|
|
value.prepareMeasuredBatch({ detail: { value: '0' } })
|
|
|
|
|
|
assert.deepEqual(value.marks, [])
|
2026-10-16 11:43:15 +08:00
|
|
|
|
})
|