This commit is contained in:
2026-08-24 19:10:22 +08:00
parent b50e46ed43
commit bb2171ac28
22 changed files with 2362 additions and 31 deletions
@@ -0,0 +1,7 @@
const http=require('http');
const fs=require('fs');
const path=require('path');
http.createServer((req,res)=>{
const file=path.join(process.cwd(), req.url==='/'?'index.html':req.url);
fs.createReadStream(file).on('error',()=>{res.statusCode=404;res.end('Not found')}).pipe(res);
}).listen(3000,'127.0.0.1');
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'totalWeightLb:203.0',
'totalWeightLb:99.9',
"totalBoxes:'总箱数'",
"totalWeight:'总重量'",
"totalBoxes:'Total cartons'",
"totalWeight:'Total weight'",
"totalBoxes:'Total de cajas'",
"totalWeight:'Peso total'",
'pickTasks.reduce((sum,task)=>sum+Number(task.palletCount||0),0)',
'pickTasks.reduce((sum,task)=>sum+Number(task.boxes||0),0)',
'pickTasks.reduce((sum,task)=>sum+Number(task.totalWeightLb||0),0)',
'${totalWeight.toFixed(1)} lb'
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing handover summary contract: $($missing -join ', ')" }
$handoverSource = [regex]::Match($html, '(?s)function handoverView\(\).*?(?=function handleHandoverPhoto)').Value
if ($handoverSource.Contains('<span>${t(''boxes'')}</span>')) {
throw 'Handover still uses the generic cartons label'
}
Write-Output 'PASS: handover summary source contract'
@@ -0,0 +1,149 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const match = html.match(/<script>([\s\S]*?)<\/script>/);
assert.ok(match, 'inline application script is missing');
const applicationSource = match[1].replace(/\s*render\(\);\s*$/, '');
const tests = [];
const runtimeTests = String.raw`
render=()=>{};
showModal=(message,onConfirm)=>{globalThis.__lastModal={message,onConfirm}};
closeModal=()=>{};
toast=()=>{};
function test(name,fn){tests.push({name,fn})}
test('consolidation ignores dropship pallet lookup and clears type inputs',()=>{
state.palletOrderType='dropship';
state.fbaPalletLabels=['D4444-TC-001'];
state.dropshipPalletLabel='D2222-TC-001';
state.palletTypeMessage={type:'error',text:'stale'};
state.fbaPalletMessage={type:'error',text:'stale'};
state.dropshipScanMessage={type:'error',text:'stale'};
startConsolidation();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
assert.equal(state.palletTypeMessage,null);
assert.equal(state.fbaPalletMessage,null);
assert.equal(state.dropshipScanMessage,null);
state.pallet={id:'DRAFT',mode:'consolidate',planId:null,targetPallets:null,destinationCode:'',destinationAddress:'',items:[]};
document.__input='D2222-TC-001';
handlePalletScan();
assert.equal(state.pallet.items.length,0,'consolidation must not import an existing pallet');
assert.equal(state.dropshipPalletLabel,'');
});
test('starting and selecting jobs reset type-specific inputs',()=>{
state.palletMode='palletize';
state.selectedPlanId='INB-260819-026';
state.fbaPalletLabels=['D4444-TC-001'];
state.dropshipPalletLabel='D2222-TC-001';
state.fbaPalletMessage={type:'error',text:'stale'};
state.dropshipScanMessage={type:'error',text:'stale'};
startPallet();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
assert.equal(state.fbaPalletMessage,null);
assert.equal(state.dropshipScanMessage,null);
state.fbaPalletLabels=['D4444-TC-002'];
state.dropshipPalletLabel='D2222-TC-001';
selectPallet('D4444-TC-003','INB-260819-026');
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-004'];
state.dropshipPalletLabel='D2222-TC-001';
document.__input='BX-88401-01';
handlePalletTopScan();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-005'];
state.dropshipPalletLabel='D2222-TC-001';
startNewConsolidation();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-006'];
state.dropshipPalletLabel='D2222-TC-001';
reconsolidatePallet('D2222-TC-019');
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
});
test('FBA selector disables occupied labels and every label for persisted pallets',()=>{
state.palletOrderType='fba';
state.pallet={id:'',mode:'palletize',planId:'INB-260819-018',items:[],persisted:false};
let markup=fbaPalletLabelSelector(plans[0]);
assert.match(markup,/D2222-TC-001[\s\S]*?<\/button>/);
const occupiedButton=markup.match(/<button[^>]*>[\s\S]*?D2222-TC-001[\s\S]*?<\/button>/)?.[0]||'';
assert.match(occupiedButton,/disabled/);
state.pallet.persisted=true;
markup=fbaPalletLabelSelector(plans[0]);
const buttons=[...markup.matchAll(/<button[^>]*class="fba-pallet-choice[^>]*>/g)].map(match=>match[0]);
assert.ok(buttons.length>1);
assert.ok(buttons.every(button=>button.includes('disabled')));
});
test('FBA and dropship validation messages render in their owning modules',()=>{
state.palletMode='palletize';
state.selectedPlanId='INB-260819-026';
state.pallet={id:'',mode:'palletize',planId:'INB-260819-026',destinationCode:'',destinationAddress:'',items:[]};
state.fbaPalletMessage={type:'error',text:'FBA_MESSAGE'};
const selector=fbaPalletLabelSelector(plans[2]);
assert.match(selector,/fba-pallet-selector[\s\S]*FBA_MESSAGE/);
state.palletOrderType='dropship';
state.fbaPalletMessage=null;
state.dropshipScanMessage={type:'error',text:'DROPSHIP_MESSAGE'};
const view=palletView();
const scanStart=view.indexOf('pallet-type-scan');
const scanEnd=view.indexOf('</section>',scanStart);
assert.ok(scanStart>=0 && view.slice(scanStart,scanEnd).includes('DROPSHIP_MESSAGE'));
});
test('FBA submission materializes every available selected label exactly once',()=>{
const plan=plans[2];
const originalPallets=state.pallets.map(pallet=>JSON.parse(JSON.stringify(pallet)));
const originalReceived=plan.receivedPallets;
const source=cargo.find(item=>item.id==='C-004');
source.palletNo='';
state.pallets=originalPallets;
state.selectedPlanId=plan.id;
state.palletMode='palletize';
state.palletOrderType='fba';
state.fbaPalletLabels=['D4444-TC-001','D4444-TC-002'];
state.pallet={id:'',mode:'palletize',planId:plan.id,targetPallets:plan.expectedPallets,destinationCode:source.destinationCode,destinationAddress:source.destinationAddress,items:[{...source}]};
submitPallet();
const created=state.pallets.filter(pallet=>['D4444-TC-001','D4444-TC-002'].includes(pallet.id));
assert.equal(created.length,2);
assert.equal(new Set(created.map(pallet=>pallet.id)).size,2);
assert.equal(created.reduce((count,pallet)=>count+pallet.items.length,0),1,'cargo must be linked once across the selected pallets');
assert.equal(source.palletNo,'D4444-TC-001');
assert.equal(plan.receivedPallets,Math.min(plan.expectedPallets,originalReceived+2));
});
for(const entry of tests){
try{entry.fn();process.stdout.write('PASS: '+entry.name+'\n')}
catch(error){process.stderr.write('FAIL: '+entry.name+'\n'+error.stack+'\n');globalThis.__failed=(globalThis.__failed||0)+1}
}
if(globalThis.__failed)throw new Error(globalThis.__failed+' runtime behavior test(s) failed');
`;
const documentStub = {
__input: '',
getElementById(id) {
if (id === 'pallet-scan' || id === 'pallet-top-scan') return { value: this.__input };
return null;
}
};
const run = new Function('assert', 'tests', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
run(assert, tests, { getItem: () => null, setItem: () => {} }, documentStub, { scrollTo: () => {} });
@@ -0,0 +1,87 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'id="pallet-top-scan"',
'function handlePalletTopScan()',
"state.page='pallet'",
"t('palletLabel')",
"t('destination')",
"t('cartonCount')",
"t('palletStatus')",
"unknownPalletScan"
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing pallet scan/list contract: $($missing -join ', ')" }
$requiredTypeContracts = @(
"palletOrderType:'fba'",
'fbaPalletLabels:[]',
"dropshipPalletLabel:''",
'function setPalletOrderType(type)',
'class="pallet-order-tabs"',
"setPalletOrderType('fba')",
"setPalletOrderType('overseas')",
"setPalletOrderType('dropship')"
)
$missingTypeContracts = $requiredTypeContracts | Where-Object { -not $html.Contains($_) }
if ($missingTypeContracts) { throw "Missing pallet order tabs: $($missingTypeContracts -join ', ')" }
$plansView = [regex]::Match($html, '(?s)function plansPalletView\(\).*?(?=function receivePhotoView)').Value
if (-not $plansView.Contains('pallet-top-scan')) { throw 'Pallet list does not expose a top scan field' }
if ($plansView.Contains("t('orderList')")) { throw 'Pallet list still presents the order-list label' }
if (-not $plansView.Contains("t('palletBoxSummary')")) { throw 'Pallet list summary label is missing' }
if (-not $plansView.Contains('const totalBoxes=pallets.length*5')) { throw 'Pallet list total carton count is missing' }
if (-not $plansView.Contains('${t(''palletBoxSummary'')}:${pallets.length}/${totalBoxes}')) { throw 'Pallet summary is missing the colon separator' }
if (-not $plansView.Contains("t('scanBoxSku')")) { throw 'Top scan does not use carton mark/SKU copy' }
if (-not $html.Contains('function handlePalletTopScan()')) { throw 'Top scan handler is missing' }
if (-not $html.Contains("c.boxNo===value||c.sku===value")) { throw 'Top scan does not resolve carton mark/SKU' }
$scanHandler = [regex]::Match($html, '(?s)function handlePalletTopScan\(\).*?(?=function receivePhotoView)').Value
if (-not $scanHandler.Contains("if(!value){state.palletTopMessage=null;state.palletMode='palletize';startPallet();return}")) { throw 'Empty scan-button click does not enter palletizing' }
if (-not $plansView.Contains('<table class="table pallet-list-table">')) { throw 'Pallet list is not rendered as a table' }
if ($plansView.Contains("t('viewPalletDetails')") -or $plansView.Contains("t('palletizeAction')")) { throw 'Pallet list still exposes a detail/action button' }
if ($plansView.Contains('${esc(pallet.plan.id)}') -or $plansView.Contains('${esc(pallet.container)}')) { throw 'Pallet list still exposes order/container information' }
$css = [regex]::Match($html, '(?s)<style id="pallet-list-toolbar">.*?</style>').Value
if (-not $css.Contains('.pallet-list-table{min-width:0')) { throw 'Pallet table still requires horizontal scrolling' }
if (-not $css.Contains('.pallet-list-table th,.pallet-list-table td{padding:8px 6px')) { throw 'Pallet table spacing is not compact' }
if (-not $css.Contains('.pallet-list-table tbody td:nth-child(-n+3){font-size:12px}')) { throw 'Primary pallet columns are not enlarged' }
if (-not $css.Contains('.pallet-list-table th:nth-child(1),.pallet-list-table td:nth-child(1){width:35%}')) { throw 'Pallet label column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(2),.pallet-list-table td:nth-child(2){width:25%}')) { throw 'Destination column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(3),.pallet-list-table td:nth-child(3){width:15%;text-align:left}')) { throw 'Carton count column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(4),.pallet-list-table td:nth-child(4){width:25%}')) { throw 'Status column width is incorrect' }
if ($html.Contains("if(state.page==='plans'||state.page==='relabel')")) { throw 'Pallet page still injects an order count' }
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
$tabsAt = $palletView.IndexOf('${palletOrderTabs()}')
$progressAt = $palletView.IndexOf('pallet-progress-card')
$cargoAt = $palletView.IndexOf('${cargoInfo}')
if ($tabsAt -lt 0 -or $progressAt -lt $tabsAt -or $cargoAt -lt $progressAt) { throw 'Pallet detail module order is incorrect' }
if (-not $palletView.Contains("const showDetails=state.palletOrderType==='overseas'")) { throw 'Detail visibility is not type-driven' }
if (-not $palletView.Contains("const scanHint=state.palletOrderType==='dropship'?t('scanPalletLabel'):t('scanBoxSku')")) { throw 'Scan hint is not type-driven' }
if (-not $palletView.Contains("t('cargoInfo')")) { throw 'Pallet detail title is not cargo information' }
if (-not $palletView.Contains("t('inboundCode')")) { throw 'Pallet detail does not show inbound code' }
if (-not $palletView.Contains("t('cartonCount')")) { throw 'Pallet detail does not show carton count' }
if ($palletView.Contains("t('palletNo')")) { throw 'Pallet detail still shows pallet number' }
if ($palletView.Contains("t('destinationAddress')")) { throw 'Pallet detail still shows destination address' }
if ($palletView.Contains("t('pieces')")) { throw 'Pallet detail still shows pieces' }
$fbaContracts = @(
'function fbaPalletLabelSelector(plan)',
'function toggleFbaPalletLabel(palletId)',
'state.fbaPalletLabels.includes(palletId)',
"state.palletOrderType==='fba'&&!state.fbaPalletLabels.length"
)
$missingFba = $fbaContracts | Where-Object { -not $html.Contains($_) }
if ($missingFba) { throw "Missing FBA multi-select behavior: $($missingFba -join ', ')" }
$dropshipContracts = @(
"state.palletOrderType==='dropship'",
'state.dropshipPalletLabel=pallet.id',
"state.palletOrderType==='dropship'&&!state.dropshipPalletLabel",
"state.palletOrderType!=='dropship'"
)
$missingDropship = $dropshipContracts | Where-Object { -not $html.Contains($_) }
if ($missingDropship) { throw "Missing dropship scan behavior: $($missingDropship -join ', ')" }
Write-Output 'PASS: pallet scan/list source contract'
@@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'selectedRelabelId:null',
'relabelPhotos:{}',
"state.page='relabel-photo'",
'function relabelPhotoView()',
'function openRelabelPhoto(planId)',
'function handleRelabelPhoto(input)',
'function submitRelabelPhoto()',
'capture="environment"',
"state.page='relabel'"
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing relabel photo page contract: $($missing -join ', ')" }
$relabelView = [regex]::Match($html, '(?s)function relabelView\(\).*?(?=function openRelabelPhoto)').Value
if (-not $relabelView.Contains("onclick=`"openRelabelPhoto('")) { throw 'Relabel list does not open the photo page' }
if ($relabelView.Contains('capture="environment"')) { throw 'Relabel list still contains an inline camera input' }
$photoView = [regex]::Match($html, '(?s)function relabelPhotoView\(\).*?(?=function handleRelabelPhoto)').Value
if (-not $photoView.Contains("!photo?'disabled':''")) { throw 'Photo submit button is not gated by a captured photo' }
if (-not $photoView.Contains("'relabel'")) { throw 'Photo page does not return to the relabel list' }
Write-Output 'PASS: relabel photo page source contract'