Files
pda-overseas-uniapp/tests/warehouse-transfer-counting.test.mjs
T
2026-11-04 19:27:34 +08:00

309 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
const source = readFileSync(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue',import.meta.url),'utf8')
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^import .+$/gm,'').replace('export default','page =')
function setup() {
const storage = new Map()
const scope = { CartonCountFields:{},PortalHeader:{},ScanTargetInput:{},session:{apiBase:'LOCAL',currentUser:{id:'OP',warehouseId:'WH'}},uni:{showModal: options=>options.success({confirm:true}),getStorageSync:key=>storage.get(key),setStorageSync:(key,value)=>storage.set(key,JSON.parse(JSON.stringify(value))),removeStorageSync:key=>storage.delete(key)} }
runInNewContext(script,scope)
const value = scope.page.data()
for(const [key,method] of Object.entries(scope.page.methods)) value[key]=method.bind(value)
for(const [key,get] of Object.entries(scope.page.computed)) Object.defineProperty(value,key,{get:()=>get.call(value)})
value.selected={transferNo:'WT1',packageCount:1,status:'WAIT_PDA_RECEIVING',items:[{id:'I1',skuCode:'A',quantity:4}],cartons:[{cartonMark:'BOX1',transferItemId:'I1',status:'EXPECTED'}]}
value.pallets=[{key:'P1',draftId:'P1',palletNo:'PALLET1',generated:true,bound:false}];value.restoreCartons();Object.assign(value.cartonBoxes[0],{length:10,width:20,height:30,weight:2});value.cartonBoxes[0].items[0].quantity=4
return {scope,value,storage}
}
test('deletion needs confirmation and cancel preserves box and measurements',async()=>{
const {value,scope}=setup(); let warning;
scope.uni.showModal=options=>{warning=options;options.success({confirm:false})};
await value.removeCarton(0);
assert.equal(value.cartonBoxes.length,1);assert.equal(value.cartonBoxes[0].weight,2);
assert.match(warning.content,/不计入本次实收/);assert.equal(warning.confirmText,'确认删除');
assert.equal(value.restorableCartons.length,0);assert.equal(value.deletingCarton,false);
})
test('deleted box survives reload and restores original identity, SKU, measurements and pallet',async()=>{
const {value}=setup(); await value.removeCarton(0);
assert.equal(value.cartonBoxes.length,0);assert.equal(value.restorableCartons.length,1);
value.restoreCartons();await value.restoreDeletedCarton();
const box=value.cartonBoxes[0];
assert.equal(box.cartonMark,'BOX1');assert.equal(box.weight,2);assert.equal(box.items[0].quantity,4);
assert.equal(box.transferPalletId,'P1');assert.equal(value.restorableCartons.length,0);
assert.equal(value.selected.packageCount,1);assert.equal(value.cartonConfirmed(box),false);
})
test('saved missing box can be restored after reopening count without increasing forecast',async()=>{
const {value,scope}=setup();
value.selected.cartons[0].status='NOT_RECEIVED';value.selected.cartons[0].countedAt='2026-10-16';
value.restoreCartons();assert.equal(value.cartonBoxes.length,0);
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:value.selected.cartons.map(box=>({...box,countedAt:null}))});
await value.restoreDeletedCarton();
assert.equal(value.cartonsCounted,false);assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');
assert.equal(value.selected.packageCount,1);assert.equal(value.restorableCartons.length,0);
value.cartonBoxes[0].weight=9;value.saveCartonDraft();value.restoreCartons();
assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');assert.equal(value.cartonBoxes[0].weight,9);
})
test('previously deleted forecast box without an archive can still be restored',async()=>{
const {value}=setup();value.cartonBoxes=[];value.saveCartonDraft();value.restoreCartons();
assert.equal(value.restorableCartons.length,1);
await value.restoreDeletedCarton();
assert.equal(value.cartonBoxes[0].cartonMark,'BOX1');assert.equal(value.selected.packageCount,1);
})
test('summary toggle preserves measurements and volume uses three decimals',()=>{
const {value}=setup()
assert.equal(value.summaryCollapsed,false)
value.summaryCollapsed=true
assert.equal(value.cartonBoxes[0].weight,2)
Object.assign(value.cartonBoxes[0],{length:10,width:10,height:6})
value.addExtraCarton()
Object.assign(value.cartonBoxes[1],{length:10,width:10,height:6,weight:2})
assert.equal(value.palletVolume(value.pallets[0]),'0.001')
assert.equal(value.totalVolume,'0.001')
assert.match(source,/aria-label="添加 SKU"/)
assert.match(source,/aria-label="删除箱子"/)
})
test('remeasure preserves saved measurements and unlocks carton editing',async()=>{
const {value,scope}=setup();
const box={...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'};
value.selected.cartons=[box]; value.cartonRequestKey='old-key';
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:[{...box,countedAt:null}]});
await value.remeasure();
assert.equal(value.cartonsCounted,false); assert.equal(value.cartonBoxes[0].weight,2);
assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.cartonRequestKey,'');
assert.equal(value.cartonCountReady,true);
})
test('bound pallet deletion retains box values and requires reassignment',async()=>{
const {value,scope}=setup();
scope.uni.showModal=options=>options.success({confirm:true});
value.pallets[0].bound=true;
scope.deleteWarehouseTransferPallet=async()=>({...value.selected,pallets:[]});
await value.removePallet(0);
assert.equal(value.cartonBoxes[0].transferPalletId,''); assert.equal(value.cartonBoxes[0].weight,2);
assert.equal(value.generatedPallets.length,0); assert.equal(value.pallets.length,1);
assert.equal(value.cartonCountReady,false); assert.equal(value.cartonPalletFilter,'');
})
test('failed pallet deletion retains binding and measurements',async()=>{
const {value,scope}=setup();
scope.uni.showModal=options=>options.success({confirm:true});
scope.deleteWarehouseTransferPallet=async()=>{throw new Error('offline')};
await value.removePallet(0);
assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.pallets[0].busy,false);
assert.equal(value.error.message,'offline');
})
test('transfer counts require valid pieces and measurements and record variance reason',()=>{
const {value}=setup();assert.equal(value.cartonCountReady,true)
value.cartonBoxes[0].items[0].quantity=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=1.5;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=4;value.cartonBoxes[0].height=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].height=30;value.addExtraCarton();Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
assert.equal(value.cartonCountReady,true);value.countDiscrepancyReason='多一箱';assert.equal(value.cartonCountReady,true)
assert.equal(value.cartonQuantity,9);assert.equal(value.cartonVolume,'0.012')
})
test('failed count unlocks the draft and each explicit retry sends current contents with a fresh key',async()=>{
const {value,scope}=setup();const requests=[]
scope.countWarehouseTransferCartons=async(no,request)=>{requests.push(request);throw new Error('offline')}
await value.confirmCartons();await value.confirmCartons()
assert.equal(requests.length,2);assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)
value.cartonBoxes[0].items[0].quantity=8;await value.confirmCartons();assert.notEqual(requests[1].idempotencyKey,requests[2].idempotencyKey)
assert.equal(requests[2].boxes[0].items[0].quantity,8);assert.equal(value.cartonBoxes[0].items[0].quantity,8)
assert.equal(value.countingCartons,false);assert.equal(value.error.message,'offline')
})
test('draft restores additional cartons and discrepancy reason without crossing tasks',()=>{
const {value}=setup();value.addExtraCarton();value.countDiscrepancyReason='超收';value.saveCartonDraft();value.restoreCartons()
assert.equal(value.cartonBoxes.length,2);assert.equal(value.countDiscrepancyReason,'超收')
value.selected={...value.selected,transferNo:'WT2'};value.restoreCartons();assert.equal(value.cartonBoxes.length,1)
})
test('received count gates pallet completion and saving cannot switch active order',()=>{
const {value}=setup();assert.match(value.validationMessage,/先完成全部箱子/)
value.selected.cartons[0].countedAt='2026-10-15';value.cartonBoxes[0].transferPalletId='FOREIGN';assert.match(value.validationMessage,/已清点实收箱数/)
value.countingCartons=true;value.backToCandidates();assert.equal(value.selected.transferNo,'WT1')
})
test('batch applies only selected received boxes of one SKU and preserves independent drafts',()=>{
const {value}=setup()
value.selected.items.push({id:'I2',skuCode:'B'})
value.addExtraCarton();value.addExtraCarton();value.addExtraCarton()
value.cartonBoxes[2].items[0].skuCode='B'
value.cartonBoxes[3].received=false
value.selectBatchCartons(true)
assert.equal(value.batchTargetBoxes.length,2)
value.toggleBatchCarton(0)
Object.assign(value.batchBox,{length:32,width:24,height:18,weight:3.2})
value.batchBox.items[0].quantity=6
assert.equal(value.batchTotalQuantity,6)
value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
assert.equal(value.cartonBoxes[2].items[0].quantity,'')
assert.equal(value.cartonBoxes[3].items[0].quantity,'')
value.batchBox.items[0].quantity=8
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
value.restoreCartons()
assert.equal(value.cartonBoxes[1].items[0].quantity,6)
assert.equal(value.batchTargetBoxes.length,0)
value.changeBatchSku(1)
assert.equal(value.batchSku,'B')
assert.equal(value.batchBox.items[0].quantity,'')
})
test('batch guards invalid quantities, busy state, submitted facts and stale mixed SKU selections',()=>{
const {value}=setup();value.selectBatchCartons(true)
Object.assign(value.batchBox,{length:32,width:24,height:18,weight:3.2})
for(const quantity of [0,1.5,2147483648]){
value.batchBox.items[0].quantity=quantity;value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
}
value.batchBox.items[0].quantity=6;value.countingCartons=true;value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.countingCartons=false;value.cartonBoxes[0].items[0].skuCode='B'
assert.equal(value.batchTargetBoxes.length,0);value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.cartonBoxes[0].items[0].skuCode='A';value.selected.cartons[0].countedAt='now';value.applyBatchCartons()
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
})
test('copy uses preceding received same SKU without sharing quantity objects or changing box identity',()=>{
const {value}=setup();value.addExtraCarton();value.addExtraCarton()
value.cartonBoxes[1].items[0].skuCode='B'
value.copyPreviousCarton(1);assert.equal(value.cartonBoxes[1].items[0].quantity,'')
const identity=value.cartonBoxes[2].localKey
value.copyPreviousCarton(2)
assert.equal(value.cartonBoxes[2].length,10)
assert.equal(value.cartonBoxes[2].items[0].quantity,4)
assert.equal(value.cartonBoxes[2].localKey,identity)
value.cartonBoxes[2].items[0].quantity=9
assert.equal(value.cartonBoxes[0].items[0].quantity,4)
value.cartonBoxes[0].received=false;assert.equal(value.previousMatchingCarton(2),undefined)
})
test('pallet totals follow actual box assignments and exclude missing boxes',()=>{
const {value}=setup();value.pallets.push({key:'P2',draftId:'P2',palletNo:'PALLET2',generated:true})
value.addExtraCarton();Object.assign(value.cartonBoxes[1],{length:40,width:20,height:10,weight:3,transferPalletId:'P2'});value.cartonBoxes[1].items[0].quantity=6
assert.equal(value.palletSummary(value.pallets[0]).cartonCount,1)
assert.equal(value.palletSummary(value.pallets[1]).volume,0.008)
assert.equal(value.totalWeight,'5.00')
value.assignCartonPallet(value.cartonBoxes[1],0)
assert.equal(value.palletSummary(value.pallets[0]).cartonCount,2)
assert.equal(value.palletSummary(value.pallets[1]).cartonCount,0)
value.cartonBoxes[1].received=false
assert.equal(value.totalWeight,'2.00')
value.cartonBoxes[0].transferPalletId='FOREIGN';assert.equal(value.cartonCountReady,false)
})
test('pallet number can be created before measurements and new boxes receive that identity',async()=>{
const {value,scope}=setup();const requests=[]
value.pallets=[{key:'NEW',generated:false,busy:false}];value.cartonBoxes[0].transferPalletId=''
const pallet={id:'CREATED',sequenceNo:1,status:'GENERATED',palletNo:'NEWPALLET'}
scope.saveWarehouseTransferPalletDraft=async(no,request)=>{requests.push(request);return {pallets:[{...pallet,status:'DRAFT'}]}}
scope.generateWarehouseTransferPallet=async()=>({pallets:[pallet]})
await value.generatePallet(value.pallets[0])
assert.equal(requests.length,1);assert.equal('lengthCm' in requests[0],false);assert.equal('grossWeightKg' in requests[0],false)
assert.equal(value.cartonBoxes[0].transferPalletId,'CREATED')
assert.equal(value.pallets[0].palletNo,'NEWPALLET')
})
// 最终完成入口需要区分无差异、取消、空白原因和网络重试。
for (const answer of [{confirm:false}, {confirm:true,content:' '}, {confirm:true,content:'少到一箱'}]) {
test('completion handles discrepancy answer '+JSON.stringify(answer),async()=>{
const {value,scope}=setup();let requests=[];let prompts=0
value.selected.items[0].quantity=5;value.selected.cartons[0].countedAt='2026-10-16'
value.pallets[0].bound=true;value.photoFileIds=[91]
scope.uni.showModal=options=>{prompts++;options.success(answer)}
scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
await value.submit()
assert.equal(prompts,1);assert.equal(value.submitting,false)
assert.equal(requests.length,answer.confirm && answer.content?.trim()?1:0)
if(requests.length){assert.equal(requests[0].discrepancyReason,'少到一箱');await value.submit();assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)}
})
}
test('matching SKU counts with different carton count submit without prompting, and missing photos block before prompting',async()=>{
const {value,scope}=setup();let prompts=0;let requests=[]
value.selected.packageCount=9;value.selected.cartons[0].countedAt='2026-10-16';value.pallets[0].bound=true
scope.uni.showModal=()=>{prompts++}
scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
await value.submit();assert.equal(requests.length,0);assert.equal(prompts,0)
value.photoFileIds=[91];await value.submit();assert.equal(prompts,0);assert.equal(requests.length,1);assert.equal(requests[0].discrepancyReason,null)
})
// 逐箱确认收起、再次编辑及草稿恢复必须保留其他箱的确认状态。
test('individual confirmation collapses only that box and resumes from draft',async()=>{
const {value,scope}=setup();value.addExtraCarton()
Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
let saves=0;scope.countWarehouseTransferCartons=async()=>{saves++;throw new Error('offline')}
await value.confirmSingleCarton(value.cartonBoxes[0])
assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true);assert.equal(value.allCartonsConfirmed,false);assert.equal(saves,0)
value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
await value.confirmSingleCarton(value.cartonBoxes[0]);await value.confirmSingleCarton(value.cartonBoxes[1])
assert.equal(value.allCartonsConfirmed,true);assert.equal(saves,1);assert.equal(value.error.message,'offline')
value.cartonBoxes[0].weight=3;assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.allCartonsConfirmed,false)
})
test('saved boxes restore collapsed and editing reopens only selected box',async()=>{
const {value,scope}=setup()
const first={...value.cartonBoxes[0],quantity:4,countedAt:'2026-10-16',status:'COUNTED'}
const second={...first,cartonMark:'BOX2'}
value.selected.cartons=[first,second];value.restoreCartons()
assert.equal(value.cartonBoxes.every(box=>value.cartonConfirmed(box)),true)
scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:value.selected.cartons.map(box=>({...box,countedAt:null}))})
await value.editCarton(value.cartonBoxes[0])
assert.equal(value.cartonsCounted,false);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
})
test('failed reopening preserves confirmed data and invalid box cannot confirm',async()=>{
const {value,scope}=setup();value.cartonBoxes[0].weight=0
await value.confirmSingleCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
value.cartonBoxes[0].weight=2;value.selected.cartons=[{...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'}];value.restoreCartons()
scope.reopenWarehouseTransferMeasurement=async()=>{throw new Error('offline')}
await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonsCounted,true);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
})
// 箱数变化不影响 SKU 门禁,异 SKU 正负差异不能按总件数抵消。
test('SKU differences compare each SKU and ignore carton count',()=>{
const {value}=setup();value.selected.packageCount=9
assert.equal(value.hasSkuReceiptDifference,false)
value.selected.items.push({id:'I2',skuCode:'B',quantity:3})
value.cartonBoxes[0].items=[{skuCode:'A',quantity:3},{skuCode:'B',quantity:4}]
assert.equal(value.cartonQuantity,value.forecastSkuQuantity)
assert.equal(value.skuReceiptDifferences.length,2)
value.cartonBoxes[0].items=[{skuCode:'A',quantity:4},{skuCode:'B',quantity:3}]
assert.equal(value.hasSkuReceiptDifference,false)
})
// 客户补箱后,新增预报箱进入PDA,原本机草稿和删除标记不丢失。
test('forecast additions merge into existing draft without losing edited boxes',async()=>{
const {value}=setup();value.cartonBoxes[0].weight=7;value.saveCartonDraft()
value.selected.packageCount=2;value.selected.cartons.push({cartonMark:'BOX2',transferItemId:'I1',status:'EXPECTED'})
value.restoreCartons();assert.equal(value.cartonBoxes.length,2);assert.equal(value.cartonBoxes[0].weight,7);assert.equal(value.cartonBoxes[1].cartonMark,'BOX2')
await value.removeCarton(0);value.restoreCartons();assert.equal(value.cartonBoxes.length,1);assert.equal(value.cartonBoxes[0].cartonMark,'BOX2')
})
test('count payload carries forecast version for stale snapshot rejection',async()=>{
const {value,scope}=setup();let payload
scope.countWarehouseTransferCartons=async(no,request)=>{payload=request;throw new Error('offline')}
await value.confirmCartons();assert.equal(payload.forecastPackageCount,1);assert.equal(payload.boxes[0].cartonMark,'BOX1')
})
test('新增预报箱无SKU绑定时恢复为可编辑箱,保留已有草稿', () => {
const { value } = setup()
value.selected.cartons.push({ cartonMark: 'NEW-UNBOUND', cartonIndex: 2, status: 'EXPECTED', transferItemId: null })
value.selected.packageCount = 2
value.restoreCartons()
const added = value.cartonBoxes.find(box => box.cartonMark === 'NEW-UNBOUND')
assert.ok(added)
assert.ok(value.declaredSkuCodes.includes(added.items[0].skuCode))
})
test('warehouse transfer errors show guidance without diagnostic identifiers', () => {
const { value: page } = setup()
assert.equal(page.simpleErrorMessage({message:'该单为仓库上门取货,请先完成车辆取货送仓\nrequestId: request-1'}), '该单为仓库上门取货,请先完成车辆取货送仓')
assert.equal(page.simpleErrorMessage({message:'操作失败(错误码:S0503;requestId:request-1;traceId:trace-1)'}), '操作失败')
})