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); });