Files
baiyue-logistics-chat/tests/frontend/customer_orders.test.cjs
T

242 lines
13 KiB
JavaScript

const {test} = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const flush = async () => { for(let i=0;i<12;i++) await Promise.resolve(); };
function page(){
class Element {
constructor(){ this.children=[]; this.attributes={}; this.hidden=false; this.dataset={}; }
set textContent(value){ this.text=value; this.children=[]; }
get textContent(){ return (this.text || '')+this.children.map(x=>x.textContent).join(''); }
set innerHTML(value){ this.children=[]; this.text=value; }
append(...nodes){ this.children.push(...nodes); }
appendChild(node){ this.append(node); }
setAttribute(key,value){ this.attributes[key]=String(value); }
focus(){ this.focused=true; }
}
const ids={}, requests=[], timers=new Map();
let timerId=0;
const el=id=>ids[id] ||= new Element();
const context=vm.createContext({document:{getElementById:el,createElement:()=>new Element()}, AbortController,
setTimeout:callback=>{const id=++timerId;timers.set(id,callback);return id;},
clearTimeout:id=>timers.delete(id)});
vm.runInContext(fs.readFileSync('static/js/customer_orders.js','utf8'),context);
const orders=context.CustomerOrders.create({text:key=>key === 'ordersProgress' ? 'Loaded {loaded} of {total}' : key,
fetch:(url,options)=>new Promise((resolve,reject)=>requests.push({url,options,resolve,reject})),
renderCard:(element,card)=>{element.textContent=card.no;},
onUnauthorized:()=>{page.expired=true;}});
return {el,orders,requests,timeout:()=>{for(const callback of Array.from(timers.values())) callback();},reply:async (i,data,status=200)=>{
requests[i].resolve({ok:status===200,status,json:async()=>data}); await flush();
}};
}
const row=(id=1)=>({id,order_no:'ORDER'+id,inbound_code:'INBOUND'+id,status:'PROCESSING'});
const counts={total:26,pending:4,processing:20,completed:1,cancelled:0,other:1};
const filter=(p,name)=>p.el('customer-orders-summary').children.find(button=>button.dataset.filter===name);
test('overview uses full server totals; a status click filters from the first page',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:1,summary:counts});
assert.match(p.el('customer-orders-toggle').textContent,/26/);
assert.match(filter(p,'processing').textContent,/20/);
filter(p,'completed').onclick();
assert.equal(p.requests[1].url,'/api/customer/orders?status=completed');
assert.equal(p.el('customer-orders-list').textContent,'');
await p.reply(1,{orders:[row(9)],next_before:null,summary:counts});
assert.equal(filter(p,'completed').attributes['aria-pressed'],'true');
assert.equal(filter(p,'all').attributes['aria-pressed'],'false');
assert.match(p.el('customer-orders-progress').textContent,/1/);
filter(p,'processing').onclick();
await p.reply(2,{orders:[row()],next_before:1,summary:counts});
p.el('customer-orders-more').onclick();
assert.equal(p.requests[3].url,'/api/customer/orders?status=processing&before=1');
await p.reply(3,{orders:[row(2)],next_before:null,summary:counts});
assert.match(p.el('customer-orders-list').textContent,/INBOUND1.*INBOUND2/);
});
test('network failure and timeout show different recoverable messages',async()=>{
const p=page();p.orders.setUser({id:1});
p.requests[0].reject(new TypeError('Failed to fetch'));await flush();
assert.equal(p.el('customer-orders-message').textContent,'ordersNetworkError');
assert.equal(p.el('customer-orders-retry').hidden,false);
p.el('customer-orders-retry').onclick();p.timeout();
assert.equal(p.requests[1].options.signal.aborted,true);
p.requests[1].reject(Object.assign(new Error('abort'),{name:'AbortError'}));await flush();
assert.equal(p.el('customer-orders-message').textContent,'ordersTimeout');
assert.equal(p.el('customer-orders-retry').hidden,false);
p.el('customer-orders-retry').onclick();
await p.reply(2,{orders:[row()],next_before:null});
assert.equal(p.el('customer-orders-message').hidden,true);
});
test('HTTP errors are classified without showing raw server output',async()=>{
for(const [status,key,retryable] of [[400,'ordersInvalidRequest',false],[403,'ordersForbidden',false],
[429,'ordersRateLimited',true],[500,'ordersServerError',true],[502,'ordersServerError',true],
[503,'ordersServerError',true],[408,'ordersTimeout',true],[504,'ordersTimeout',true],[418,'ordersFailed',true]]){
const p=page();p.orders.setUser({id:1});
await p.reply(0,{error:'private traceback with secret'},status);
assert.equal(p.el('customer-orders-message').textContent,key,'status '+status);
assert.equal(p.el('customer-orders-retry').hidden,!retryable,'status '+status);
assert.doesNotMatch(p.el('customer-orders-message').textContent,/private|secret/);
}
});
test('missing order directs the customer back to the list instead of retrying a missing record',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:null,summary:counts});
p.el('customer-orders-list').children[0].onclick();
await p.reply(1,{error:'not found or forbidden'},404);
assert.equal(p.el('customer-orders-message').textContent,'ordersUnavailable');
assert.equal(p.el('customer-orders-back').hidden,false);
assert.equal(p.el('customer-orders-retry').hidden,true);
p.el('customer-orders-back').onclick();
assert.equal(p.el('customer-orders-message').hidden,true);
assert.equal(p.el('customer-orders-list').hidden,false);
});
test('malformed successful responses are not reported as a network outage',async()=>{
const p=page();p.orders.setUser({id:1});
p.requests[0].resolve({ok:true,status:200,json:async()=>{throw new SyntaxError('Unexpected <');}});await flush();
assert.equal(p.el('customer-orders-message').textContent,'ordersInvalidResponse');
p.el('customer-orders-retry').onclick();
await p.reply(1,{orders:'invalid'});
assert.equal(p.el('customer-orders-message').textContent,'ordersInvalidResponse');
});
test('an error page from a proxy uses HTTP status even when the body is not JSON',async()=>{
const p=page();p.orders.setUser({id:1});
let parsed=false;
p.requests[0].resolve({ok:false,status:502,json:async()=>{parsed=true;throw new SyntaxError('html');}});
await flush();
assert.equal(parsed,false);
assert.equal(p.el('customer-orders-message').textContent,'ordersServerError');
});
test('switching filters ignores late results; logout erases counts and selection',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:null,summary:counts});
filter(p,'completed').onclick();filter(p,'pending').onclick();
await p.reply(2,{orders:[],next_before:null,summary:counts});
await p.reply(1,{orders:[row(9)],next_before:null,summary:{...counts,total:999}});
assert.doesNotMatch(p.el('customer-orders-toggle').textContent,/999/);
assert.equal(p.el('customer-orders-list').textContent,'');
assert.equal(p.el('customer-orders-message').textContent,'ordersFilterEmpty');
p.orders.setUser(null);
assert.equal(p.el('customer-orders-summary').hidden,true);
assert.doesNotMatch(p.el('customer-orders-summary').textContent,/26|20/);
});
test('login automatically opens list; collapse/reopen and paging keep drafts outside panel untouched',async()=>{
const p=page();p.orders.setUser({id:1});
assert.equal(p.el('customer-orders').hidden,false);
assert.equal(p.el('customer-orders-toggle').attributes['aria-expanded'],'true');
await p.reply(0,{orders:[row()],next_before:1});
assert.match(p.el('customer-orders-list').textContent,/INBOUND1/);
p.el('customer-orders-toggle').onclick();
assert.equal(p.el('customer-orders-body').hidden,true);
p.el('customer-orders-toggle').onclick();
p.el('customer-orders-more').onclick();
assert.equal(p.requests[1].url,'/api/customer/orders?before=1');
await p.reply(1,{orders:[row(2)],next_before:null});
assert.match(p.el('customer-orders-list').textContent,/INBOUND1.*INBOUND2/);
assert.equal(p.el('customer-orders-more').hidden,true);
});
test('logout clears list and ignores delayed list/detail responses from a previous account',async()=>{
const p=page();p.orders.setUser({id:1});p.orders.setUser(null);
await p.reply(0,{orders:[row()],next_before:null});
assert.equal(p.el('customer-orders').hidden,true);
assert.equal(p.el('customer-orders-list').textContent,'');
p.orders.setUser({id:2});await p.reply(1,{orders:[row(2)],next_before:null});
p.el('customer-orders-list').children[0].onclick();
p.orders.setUser(null);p.orders.setUser({id:3});
await p.reply(2,{card:{no:'private-old-account'}});
assert.doesNotMatch(p.el('customer-orders-detail').textContent,/private/);
p.orders.setUser(null);
await p.reply(3,{orders:[],next_before:null});
});
test('exact order detail and back preserve list without a new AI request',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row(9)],next_before:null});
p.el('customer-orders-list').children[0].onclick();
assert.equal(p.requests[1].url,'/api/customer/orders/9');
await p.reply(1,{card:{no:'ORDER9'}});
assert.equal(p.el('customer-orders-detail').textContent,'ORDER9');
p.el('customer-orders-back').onclick();
assert.equal(p.el('customer-orders-list').hidden,false);
assert.equal(p.requests.length,2);
});
test('failed request offers retry; expired login immediately clears protected data',async()=>{
const p=page();p.orders.setUser({id:1});
p.requests[0].reject(new Error('offline'));await flush();
assert.equal(p.el('customer-orders-retry').hidden,false);
p.el('customer-orders-retry').onclick();
await p.reply(1,{orders:[row()],next_before:null});
p.el('customer-orders-refresh').onclick();
await p.reply(2,{error:'customer_login_required'},401);
assert.equal(p.el('customer-orders').hidden,true);
assert.equal(p.el('customer-orders-list').textContent,'');
});
test('refresh cancels pagination; stale errors cannot replace the refreshed list',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:1});
p.el('customer-orders-more').onclick();
p.el('customer-orders-refresh').onclick();
await p.reply(2,{orders:[row(3)],next_before:null});
p.requests[1].reject(new Error('late error'));await flush();
assert.equal(p.el('customer-orders-retry').hidden,true);
assert.doesNotMatch(p.el('customer-orders-list').textContent,/INBOUND1/);
assert.match(p.el('customer-orders-list').textContent,/INBOUND3/);
});
test('automatic refresh keeps the filter, old rows, scroll and collapsed state until success',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:null,summary:counts});
filter(p,'processing').onclick();
await p.reply(1,{orders:[row(2)],next_before:2,summary:counts});
p.el('customer-orders-body').scrollTop=85;
p.el('customer-orders-toggle').onclick();
p.orders.refresh();p.orders.refresh();
assert.equal(p.requests.length,3);
assert.equal(p.requests[2].url,'/api/customer/orders?status=processing');
assert.match(p.el('customer-orders-list').textContent,/INBOUND2/);
await p.reply(2,{orders:[row(3)],next_before:null,summary:{...counts,total:27,processing:21}});
assert.match(p.el('customer-orders-toggle').textContent,/27/);
assert.match(p.el('customer-orders-list').textContent,/INBOUND3/);
assert.equal(p.el('customer-orders-body').scrollTop,85);
assert.equal(p.el('customer-orders-body').hidden,true);
assert.equal(filter(p,'processing').attributes['aria-pressed'],'true');
});
test('failed automatic refresh retains previous data and retry can update it',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:1,summary:counts});
p.orders.refresh();p.requests[1].reject(new Error('offline'));await flush();
assert.match(p.el('customer-orders-list').textContent,/INBOUND1/);
assert.match(p.el('customer-orders-toggle').textContent,/26/);
assert.equal(p.el('customer-orders-retry').hidden,false);
p.el('customer-orders-retry').onclick();
await p.reply(2,{orders:[row(2)],next_before:null,summary:{...counts,total:27}});
assert.match(p.el('customer-orders-list').textContent,/INBOUND2/);
});
test('automatic refresh keeps the open tracking detail and logout invalidates late responses',async()=>{
const p=page();p.orders.setUser({id:1});
await p.reply(0,{orders:[row()],next_before:null,summary:counts});
p.el('customer-orders-list').children[0].onclick();
await p.reply(1,{card:{no:'ORDER1'}});
p.orders.refresh();
await p.reply(2,{orders:[row(2)],next_before:null,summary:{...counts,total:27}});
assert.equal(p.el('customer-orders-detail').textContent,'ORDER1');
assert.equal(p.el('customer-orders-list').hidden,true);
p.el('customer-orders-back').onclick();
assert.match(p.el('customer-orders-list').textContent,/INBOUND2/);
p.orders.refresh();p.orders.setUser(null);
await p.reply(3,{orders:[row(99)],summary:counts});
assert.equal(p.el('customer-orders-list').textContent,'');
assert.equal(p.el('customer-orders').hidden,true);
});