import {test} from 'node:test'; import assert from 'node:assert/strict'; import {runInNewContext} from 'node:vm'; import {dashboardApp} from '../src/dashboard-client.js'; const clientScript = process.env.DASHBOARD_BUNDLE ? await (await (await import('../dist/index.js')).default.fetch(new Request('https://telemetry.example/dashboard/app.js'), {})).text() : `(${dashboardApp.toString()})();`; // Minimal DOM boundary for client behavior; not a browser layout test. class Element { constructor(tag = 'div') { this.tagName=tag.toUpperCase(); this.children=[]; this.listeners={}; this.dataset={}; this.attributes={}; this.hidden=false; this.value=''; this.open=false; this._text=''; } set textContent(value) { this._text=String(value); this.children=[]; } get textContent() { return this._text+this.children.map(x=>x.textContent).join(' '); } set innerHTML(_) { throw new Error('HTML interpolation forbidden'); } append(...nodes) { this.children.push(...nodes); } replaceChildren(...nodes) { this._text=''; this.children=nodes; } addEventListener(type, handler) { this.listeners[type]=handler; } setAttribute(key,value) { this.attributes[key]=value; } removeAttribute(key) { delete this.attributes[key]; } closest(selector) { return selector==='button'&&this.tagName==='BUTTON'?this:null; } querySelector(selector) { return descendants(this).find(x=>x.tagName.toLowerCase()===selector)||null; } showModal() { this.open=true; } close() { this.open=false; this.listeners.close?.(); } focus() {} } const descendants = node => node.children.flatMap(child=>[child,...descendants(child)]); const tick = async () => { for(let i=0;i<8;i++) await new Promise(resolve=>setImmediate(resolve)); }; function fixture() { const ids=Object.fromEntries(['token','login','login-form','login-error','app','metrics','content','updated','error','scope-label','clear-scope','tabs','refresh','logout','mission-dialog','mission-heading','mission-body','close-dialog'].map(id=>[id,new Element()])); ids['login-form'].append(new Element('button')); ids.app.hidden=true; for(const view of ['overview','servers','missions','players']) { const node=new Element('button'); node.dataset.view=view; ids.tabs.append(node); } const sid='11111111-1111-4111-8111-111111111111',mid='22222222-2222-4222-8222-222222222222',malicious=''; const summary={generatedAt:1793361000,totals:{servers:1,missions:1,events:3,wins:1,losses:0,unfinished:0,partialMissions:0,winRate:100,lastReceivedAt:1793361000},outcomes:[{outcome:'victory',missions:1}],maps:[{terrain:'Everon',missions:1,wins:1,losses:0,unfinished:0}]}; const event={serverId:sid,missionId:mid,serverName:malicious,terrain:'Everon',outcome:'victory',phase:5,elapsedSeconds:120,occurredAt:1793360000,connectedPlayers:1,peakPlayers:1,gameVersion:'1.6',scenario:'Escape',previousOutcome:'',reason:malicious,partial:false,droppedEvents:0,mods:[{name:malicious,version:'1',id:'mod'}],settings:{roadblockCount:4},players:[{id:sid,name:malicious,authenticated:true,connected:true,participated:true,escaped:true,connectedSeconds:120,kills:2,friendlyKills:0}]}; const requests=[]; let fail=false; const document={getElementById:id=>ids[id],createElement:tag=>new Element(tag),createTextNode:text=>{const node=new Element('#text');node.textContent=text;return node;},querySelectorAll:()=>ids.tabs.children,hidden:false}; const fetch=async(path,options)=>{ requests.push({path,options}); if(fail)return{ok:false,status:503,json:async()=>({error:'Temporarily unavailable'})}; if(options.headers.Authorization!=='Bearer valid-read-token-0123456789abcdef')return{ok:false,status:401,json:async()=>({})}; let data; if(path.startsWith('/api/dashboard'))data=summary; else if(path.startsWith('/api/servers'))data={servers:[{serverId:sid,name:malicious,wins:1,losses:0,missions:1,unfinished:0,lastReceivedAt:1793361000,lastEventType:'heartbeat',lastReportedPlayers:1,lastMissionId:mid,terrain:'Everon',active:1,registration:'automatic'}],nextBefore:7}; else if(path.includes('/events?'))data={events:[{receivedAt:1793361000,event}],nextBefore:null}; else if(path.startsWith(`/api/missions/${sid}/`))data={receivedAt:1793361000,event}; else if(path.startsWith('/api/missions?'))data={missions:[{...event,receivedAt:1793361000}],nextBefore:null}; else if(path.startsWith('/api/players'))data={players:[{playerId:sid,attempts:1,escapes:1,didNotEscape:0,bestEscapeSeconds:120,kills:2,missionsWithKills:1,friendlyKills:0,missionsWithFriendlyKills:1}],nextAfter:null}; else throw new Error('Unexpected path '+path); return{ok:true,status:200,json:async()=>data}; }; runInNewContext(clientScript,{document,Node:Element,fetch,AbortController,URLSearchParams,Date,setInterval:()=>{throw new Error("Automatic refresh must remain disabled");},setTimeout,URL,Blob}); const click=async(node,target=node)=>{node.listeners.click?.({target});await tick();}; const button=(root,text)=>{const node=descendants(root).find(x=>x.tagName==='BUTTON'&&x.textContent===text);assert.ok(node,`Button ${text}`);return node;}; const login=async token=>{ids.token.value=token;await ids['login-form'].listeners.submit({preventDefault(){},target:ids['login-form']});await tick();}; return{ids,requests,sid,malicious,click,button,login,tab:async view=>click(ids.tabs,ids.tabs.children.find(x=>x.dataset.view===view)),fail:()=>{fail=true;}}; } test('client login, scope, pagination, mission details, safe text and logout',async()=>{ const f=fixture();await f.login('wrong-token');assert.match(f.ids['login-error'].textContent,/Token rejected/);assert.equal(f.ids.app.hidden,true); await f.login('valid-read-token-0123456789abcdef');assert.equal(f.ids.app.hidden,false);assert.equal(f.ids.token.value,'');assert.match(f.ids.metrics.textContent,/100%/); await f.tab('servers');assert.match(f.ids.content.textContent,/Last received/);assert.ok(f.ids.content.textContent.includes(f.malicious));assert.equal(descendants(f.ids.content).some(x=>x.tagName==='IMG'),false); await f.click(f.button(f.ids.content,'Next →'));assert.ok(f.requests.some(r=>r.path.includes('before=7'))); await f.click(f.button(f.ids.content,f.malicious));assert.ok(f.requests.some(r=>r.path.startsWith('/api/missions?')&&r.path.includes(`serverId=${f.sid}`))); await f.click(f.button(f.ids.content,'Everon'));assert.equal(f.ids['mission-dialog'].open,true);assert.match(f.ids['mission-body'].textContent,/Mission settings/);assert.match(f.ids['mission-body'].textContent,/Event history/);assert.ok(f.ids['mission-body'].textContent.includes(f.malicious)); await f.click(f.ids['close-dialog']);assert.equal(f.ids['mission-dialog'].open,false); await f.tab('players');assert.match(f.ids.content.textContent,/1\/1 missions reported this counter/); await f.click(f.ids.logout);assert.equal(f.ids.app.hidden,true);assert.equal(f.ids.content.textContent,'');assert.equal(f.ids['mission-body'].textContent,''); for(const request of f.requests)assert.ok(!request.path.includes('token')); }); test('failed refresh removes stale records and explains retry',async()=>{ const f=fixture();await f.login('valid-read-token-0123456789abcdef');f.fail();await f.click(f.ids.refresh); assert.equal(f.ids.error.hidden,false);assert.match(f.ids.updated.textContent,/Refresh failed/);assert.equal(f.ids.metrics.textContent,'');assert.match(f.ids.content.textContent,/Use Refresh/); });