import test from 'node:test'; import assert from 'node:assert/strict'; import {build} from 'esbuild'; const result=await build({entryPoints:[new URL('../src/core/fleet/roverHoldInput.ts',import.meta.url).pathname],bundle:true,write:false,platform:'node',format:'esm'}); const {bindRoverHoldInput}=await import('data:text/javascript;base64,'+Buffer.from(result.outputFiles[0].contents).toString('base64')); class Events { listeners=new Map();timers=new Map();next=0; addEventListener(type,fn){if(!this.listeners.has(type))this.listeners.set(type,new Set());this.listeners.get(type).add(fn);} removeEventListener(type,fn){this.listeners.get(type)?.delete(fn);} emit(type,detail={}){const e={target:this,isTrusted:true,preventDefault(){},...detail};for(const fn of [...(this.listeners.get(type)??[])])fn(e);} setInterval(fn){const id=++this.next;this.timers.set(id,fn);return id;} clearInterval(id){this.timers.delete(id);} tick(){for(const fn of this.timers.values())fn();} } function fixture(mode='arcade'){ let time=0,focus=true;const win=new Events(),doc=new Events();doc.defaultView=win;doc.hidden=false;doc.hasFocus=()=>focus; const scope={ownerDocument:doc,closest:()=>null},child={closest:()=>null};scope.contains=x=>x===scope||x===child;doc.activeElement=scope; const states=[],demands=[];let stops=0,pauses=0; const binding=bindRoverHoldInput(scope,mode,{held:k=>states.push([...k]),demand:d=>demands.push(d),stop:()=>stops++,pause:()=>{pauses++;demands.push({left:0,right:0});}},()=>time); const key=(code,repeat=false)=>win.emit('keydown',{code,repeat,target:scope}); return {win,doc,scope,child,binding,states,demands,key,stops:()=>stops,pauses:()=>pauses,advance:ms=>{time+=ms;win.tick();},loseFocus:()=>{focus=false;},restoreFocus:()=>{focus=true;},up:code=>win.emit('keyup',{code,target:scope})}; } test('blur clears D, ignores a late repeat, but accepts a fresh press without rearming',()=>{ const f=fixture();f.key('KeyD');assert.deepEqual(f.demands.at(-1),{left:1,right:-1}); f.win.emit('blur');assert.equal(f.pauses(),1);assert.equal(f.stops(),0);assert.deepEqual(f.states.at(-1),[]); assert.deepEqual(f.demands.at(-1),{left:0,right:0});const count=f.demands.length; f.key('KeyD',true);assert.equal(f.demands.length,count); f.key('KeyW');assert.deepEqual(f.demands.at(-1),{left:1,right:1});assert.equal(f.binding.valid(),true);f.binding.dispose(); }); test('heartbeat validity catches missing blur event using document focus',()=>{ const f=fixture();f.key('KeyD');f.loseFocus();assert.equal(f.binding.valid(),false);assert.equal(f.pauses(),1); f.restoreFocus();assert.equal(f.binding.valid(),true);f.binding.dispose(); }); test('focus polling catches missing event or moving to another control',()=>{ const f=fixture();f.key('KeyW');f.doc.activeElement={};f.advance(50);assert.equal(f.pauses(),1);f.binding.dispose(); }); test('lost keyup AND missing focus notifications still expire repeated D',()=>{ const f=fixture();f.key('KeyD');f.advance(500);f.key('KeyD',true);f.advance(299);assert.equal(f.pauses(),0); f.advance(1);assert.equal(f.pauses(),1);assert.deepEqual(f.states.at(-1),[]);f.key('KeyD',true);assert.equal(f.pauses(),1);f.binding.dispose(); }); test('first press allows OS repeat delay but never an indefinite hold',()=>{ const f=fixture();f.key('KeyW');f.advance(999);assert.equal(f.pauses(),0);f.advance(1);assert.equal(f.pauses(),1);f.binding.dispose(); }); test('continuous repeat renews hold; keyup immediately sends neutral',()=>{ const f=fixture();f.key('KeyW');f.advance(600); for(let i=0;i<30;i++){f.key('KeyW',true);f.advance(100);} assert.equal(f.pauses(),0);f.up('KeyW');assert.deepEqual(f.demands.at(-1),{left:0,right:0});f.advance(1200);assert.equal(f.pauses(),0);f.binding.dispose(); }); test('diagonal input survives single-key OS repeat; release recomputes demand',()=>{ const f=fixture();f.key('KeyW');f.key('KeyA'); for(let i=0;i<20;i++){f.key('KeyA',true);f.advance(100);} assert.equal(f.pauses(),0);assert.deepEqual(f.demands.at(-1),{left:0,right:1}); f.up('KeyA');assert.deepEqual(f.demands.at(-1),{left:1,right:1});f.up('KeyW');f.binding.dispose(); }); test('repeat without a fresh press and untrusted events never command motion',()=>{ const f=fixture();f.key('KeyD',true);f.win.emit('keydown',{code:'KeyW',isTrusted:false,target:f.scope});assert.equal(f.demands.length,0);f.binding.dispose(); }); test('focus inside the view is allowed; leaving or hiding it stops',()=>{ for(const event of ['focusout','visibilitychange','pagehide','outside']){ const f=fixture();f.key('KeyW');f.doc.emit('focusout',{target:f.scope,relatedTarget:f.child});assert.equal(f.pauses(),0); if(event==='focusout')f.doc.emit(event,{target:f.scope,relatedTarget:null}); else if(event==='visibilitychange'){f.doc.hidden=true;f.doc.emit(event);} else if(event==='pagehide')f.win.emit(event); else f.doc.emit('pointerdown',{target:{}}); assert.equal(event==='pagehide'?f.stops():f.pauses(),1,event);f.binding.dispose(); } }); test('pointer capture lost or cancelled releases every input',()=>{ for(const mode of ['arcade','tank']){ const f=fixture(mode);f.binding.pointerDown(1,mode==='arcade'?'KeyW':'KeyQ');f.binding.pointerCancel(1); assert.equal(f.pauses(),1);assert.deepEqual(f.states.at(-1),[]);f.binding.dispose(); } }); test('document pointerup releases even if button handler does not receive it',()=>{ const f=fixture();f.binding.pointerDown(1,'KeyD');f.doc.emit('pointerup',{pointerId:1});assert.deepEqual(f.demands.at(-1),{left:0,right:0});assert.equal(f.pauses(),0);f.binding.dispose(); }); test('modifier shortcuts pause; cleanup removes listeners, timer and held demand',()=>{ const f=fixture();f.key('KeyD');f.win.emit('keydown',{code:'Tab',metaKey:true,target:f.scope});assert.equal(f.pauses(),1);f.binding.dispose(); assert.deepEqual(f.demands.at(-1),{left:0,right:0});assert.equal(f.win.timers.size,0); assert.equal([...f.win.listeners.values(),...f.doc.listeners.values()].reduce((sum,x)=>sum+x.size,0),0); }); test('Space and Escape remain explicit session stops',()=>{ for(const code of ['Space','Escape']){const f=fixture();f.key('KeyW');f.key(code);assert.equal(f.stops(),1);assert.equal(f.binding.valid(),false);f.binding.dispose();} });