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 helperSource = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8') const helpers = await import('data:text/javascript;base64,' + Buffer.from(helperSource).toString('base64')) const body = source.split('')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return') function setup(confirm = false) { const calls = [], errors = [] const uni = { showModal(options) { options.success({ confirm }) } } const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'requiresReturnPack', 'returnPackFacts', 'completeReturnRelabelSku', 'warehouseIdempotencyKey', 'releaseWarehouseKey', 'uni', body)({}, {}, {}, {}, helpers.requiresReturnPack, helpers.returnPackFacts, async (no, payload) => { calls.push(payload); return value.current }, () => ({ value: 'key', signature: 'signature' }), () => {}, uni) const item = (id, mark) => ({ itemId: id, returnCartonId: id, cartonMark: mark, returnQuantity: 10, operationType: 'RETURN_BULK_SKU_LABEL', skuCode: 'SKU', newSkuCode: 'TARGET', expectedPackages: 4, relabelledPackages: 0, skippedPackages: 0, exceptionPackages: 0 }) const value = { current: { relabelNo: 'RL', version: 1, items: [item('1', 'BOX-A'), item('2', 'BOX-B')], history: [] }, busy: false, allProcessed: true, remark: '', photoFileIds: ['photo'], returnCartonCode: '', verifiedReturnCarton: '', returnSkuCode: '', bulkQuantities: {}, returnTargetLabels: {}, setError(error) { errors.push(error.message) }, applyCurrent(result) { this.current = result }, async run(action, work) { await work() } } for (const [name, fn] of Object.entries(component.methods)) if (!value[name]) value[name] = fn.bind(value) for (const name of ['isReturnTask', 'isReturnBulk', 'returnBulkSources', 'returnBulkSkuGroups', 'requiresPalletBinding', 'palletTask', 'completionBlockReason']) Object.defineProperty(value, name, { get: component.computed[name].bind(value) }) return { value, calls, errors } } test('SKU alone cannot register or allocate unscanned sources', async () => { const { value, calls, errors } = setup() value.verifyReturnSku('SKU'); assert.match(errors.pop(), /先确认来源/) value.verifyReturnCarton('BOX-A'); value.verifyReturnSku('SKU') assert.deepEqual(value.returnBulkSources.map(item => item.cartonMark), ['BOX-A']) value.returnTargetLabels['2'] = 'TARGET'; value.bulkQuantities['2'] = '4' await value.completeBulkSku(value.current.items[1]); assert.equal(calls.length, 0) }) test('quantity is explicit, integral and source bounded; wrong target cannot write', async () => { const { value, calls } = setup(); value.verifyReturnCarton('BOX-A'); value.verifyReturnSku('SKU') const item = value.current.items[0]; value.returnTargetLabels['1'] = 'TARGET' for (const quantity of ['', '1.5', '5', '0']) { value.bulkQuantities['1'] = quantity; await value.completeBulkSku(item) } assert.equal(calls.length, 0) value.bulkQuantities['1'] = '2'; value.returnTargetLabels['1'] = 'WRONG'; await value.completeBulkSku(item); assert.equal(calls.length, 0) value.returnTargetLabels['1'] = 'TARGET'; await value.completeBulkSku(item) assert.deepEqual(calls, [{ itemId: '1', cartonMark: 'BOX-A', skuCode: 'SKU', quantity: 2, expectedVersion: 1, idempotencyKey: 'key' }]) }) test('all action needs confirmation and rescanning clears quantity and target', async () => { for (const confirm of [false, true]) { const { value } = setup(confirm); value.verifyReturnCarton('BOX-A'); await value.fillAllReturn(value.current.items[0]); assert.equal(value.bulkQuantities['1'], confirm ? '4' : undefined); value.verifyReturnCarton('BOX-B'); assert.deepEqual(value.bulkQuantities, {}); assert.deepEqual(value.returnTargetLabels, {}) } }) test('normal return SKU rejects incomplete approved target before server scan', async () => { const { value, errors } = setup(); const item = { ...value.current.items[0], operationType: 'SKU_LABEL', expectedPackages: 1, skuItems: [{ skuCode: 'SKU', newSkuCode: 'TARGET', quantity: 3 }] } value.current.items = [item]; value.pendingSkuRows = [{ item, sku: item.skuItems[0], index: 0 }] item.skuItems[0].newSkuCode = ''; value.quickSku = 'SKU'; await value.commitSku(value.pendingSkuRows[0]); assert.match(errors.pop(), /申请件数缺失/) }) test('packing uses SKU pieces instead of box count and preserves both sources', () => { const task = { items: [{ returnCartonId: '1', cartonMark: 'A', operationType: 'SKU_LABEL', expectedPackages: 1, returnQuantity: 8, skuItems: [{ skuCode: 'SKU', quantity: 3 }] }, { returnCartonId: '2', cartonMark: 'B', operationType: 'SKU_LABEL', expectedPackages: 1, returnQuantity: 2, skuItems: [{ skuCode: 'SKU', quantity: 2 }] }] } assert.deepEqual(helpers.returnDeductions(task), [{ cartonMark: 'A', skuCode: 'SKU', quantity: 3 }, { cartonMark: 'B', skuCode: 'SKU', quantity: 2 }]); assert.equal(helpers.requiresReturnPack(task), true) task.items[0].returnQuantity = 3; assert.equal(helpers.requiresReturnPack(task), false) }) test('only truck requires return pallet binding and counts physical new boxes', () => { const { value } = setup(); for (const method of [null, 'EXPRESS', 'EXPRESS_PICKUP', 'TRUCK']) { value.current.deliveryMethod = method; assert.equal(value.requiresPalletBinding, method === 'TRUCK') } value.current.items.forEach(item => { item.cartonMark = 'NEW'; item.relabelledPackages = item.expectedPackages }) assert.equal(value.palletTask.items.length, 1); assert.equal(value.palletTask.items[0].expectedPackages, 1) }) test('pack recovery ignores previous rework round', () => { const task = { reviewedAt: 'ROUND2', history: [{ action: 'RETURN_PACK', detail: JSON.stringify({ round: 'INITIAL', cartonMark: 'OLD' }) }, { action: 'RETURN_PACK', detail: JSON.stringify({ round: 'ROUND2', cartonMark: 'NEW' }) }] } assert.deepEqual(helpers.returnPackFacts(task).map(row => row.cartonMark), ['NEW']) }) test('partial return completion requires current-round independent box then truck binding and photo', () => { const { value } = setup() value.current.items.forEach(item => { item.relabelledPackages = item.expectedPackages }) value.current.relabelledPackages = 8 value.current.deliveryMethod = 'EXPRESS' assert.match(value.completionBlockReason, /独立新箱/) value.current.history.push({ action: 'RETURN_PACK', detail: JSON.stringify({ round: 'INITIAL', cartonMark: 'NEW' }) }) value.current.items.forEach(item => { item.cartonMark = 'NEW'; item.returnQuantity = 8 }) assert.equal(value.completionBlockReason, '') value.current.deliveryMethod = 'TRUCK' assert.match(value.completionBlockReason, /组托/) value.current.history.push({ action: 'PALLET_BIND', detail: JSON.stringify({ round: 'INITIAL', cartonMarks: ['NEW'] }) }) assert.equal(value.completionBlockReason, '') value.photoFileIds = [] assert.match(value.completionBlockReason, /照片/) })