Emberveil source

tests/mobile-controls.mjs

import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
const moduleAt=async name=>import('data:text/javascript;base64,'+Buffer.from(await readFile(new URL('../mobile/public/'+name,import.meta.url))).toString('base64'));
const {bindTouch,viewportSize}=await moduleAt('touch.js');
const {createSocketBridge}=await moduleAt('network.js');
let checks=0;const check=(value,message)=>{assert.ok(value,message);checks++;};
class Socket extends EventTarget {
 static instances=[];
 constructor(url){super();this.url=url;this.readyState=0;this.bufferedAmount=0;Socket.instances.push(this);}
 emit(name,data){const e=new Event(name);if(data!==undefined)e.data=data;this.dispatchEvent(e);}
 open(){this.readyState=1;this.emit('open');}
 close(){this.readyState=3;this.emit('close');}
 send(bytes){if(this.fail)throw Error('closed');this.sent=bytes;}
 receive(bytes){this.emit('message',new Uint8Array(bytes).buffer);}
}
const bridge=createSocketBridge('wss://example.test/mobile/socket',{Socket}),n=bridge.natives;
const connect=async port=>{const p=n.Java_BrowserSocket_connectBrowser(null,port);Socket.instances.at(-1).open();return p;};
check(await n.Java_BrowserSocket_connectBrowser(null,1)===-1,'only game/cache ports');
const id=await connect(43594),s=Socket.instances.at(-1);
check(s.url==='wss://example.test/mobile/socket/game','fixed game endpoint');
s.receive([1,2,255]);s.receive([4,5]);
check(await n.Java_BrowserSocket_availableBrowser(null,id)===5,'queued byte count');
assert.deepEqual([...await n.Java_BrowserSocket_readBrowser(null,id,2,50)],[1,2]);checks++;
s.close();assert.deepEqual([...await n.Java_BrowserSocket_readBrowser(null,id,10,50)],[-1,4,5]);checks++;
check(await n.Java_BrowserSocket_availableBrowser(null,id)===-1,'EOF after buffered bytes');
check(await n.Java_BrowserSocket_readBrowser(null,id,10,50)===null,'EOF read');
const id2=await connect(43595),s2=Socket.instances.at(-1);
check(s2.url.endsWith('/cache'),'fixed cache endpoint');
const waiting=n.Java_BrowserSocket_readBrowser(null,id2,10,100);s2.receive([6,7]);
assert.deepEqual([...await waiting],[6,7]);checks++;
check(await n.Java_BrowserSocket_writeBrowser(null,id2,new Int8Array([8,9,10]),1,2),'send succeeds');
assert.deepEqual([...s2.sent],[9,10]);checks++;
s2.fail=true;check(!await n.Java_BrowserSocket_writeBrowser(null,id2,new Int8Array([1]),0,1),'send failure becomes socket failure');
const id3=await connect(43594);const waitingClose=n.Java_BrowserSocket_readBrowser(null,id3,10,100);
bridge.closeAll();check(await waitingClose===null,'page close releases pending read');
check(await n.Java_BrowserSocket_connectBrowser(null,43594)===-1,'page closed prevents reconnect');
bridge.resume();const pending=n.Java_BrowserSocket_connectBrowser(null,43594);bridge.closeAll();check(await pending===-1,'pending connection cancelled');
bridge.resume();const id4=await connect(43594);check(id4>0,'page restore reconnects');await n.Java_BrowserSocket_closeBrowser(null,id4);
const handlers={},events=[];let timer,options=false;
const surface={addEventListener:(name,fn)=>handlers[name]=fn,setPointerCapture(){},getBoundingClientRect:()=>({left:10,top:20,width:382.5,height:251.5})};
const cancel=bindTouch(surface,{ready:()=>true,options:()=>options,clearOptions:()=>options=false,point:(...a)=>events.push(['point',...a]),gesture:(...a)=>events.push(['gesture',...a]),zoom:(...a)=>events.push(['zoom',...a]),wheel:(...a)=>events.push(['wheel',...a])},{setTimer:fn=>(timer=fn,1),clearTimer:()=>timer=null});
function emit(name,x,y,id=1,extra={}){handlers[name]({clientX:x,clientY:y,pointerId:id,button:0,preventDefault(){},...extra});}
emit('pointerdown',60,70);emit('pointerup',60,70);assert.deepEqual(events.pop(),['point',100,100,1]);checks++;
emit('pointerdown',60,70);timer();emit('pointerup',60,70);assert.deepEqual(events.pop(),['point',100,100,2]);check(events.length===0,'long press has no duplicate tap');
emit('pointerdown',60,70);emit('pointermove',60,100);emit('pointerup',60,100);assert.deepEqual(events.pop(),['gesture',100,100,0,60]);check(events.length===0,'drag does not click');
emit('pointerdown',60,70);emit('pointerdown',100,70,2);emit('pointermove',140,70,2);emit('pointerup',140,70,2);emit('pointerup',60,70);assert.deepEqual(events.pop(),['zoom',-1]);check(events.length===0,'pinch does not click');
options=true;emit('pointerdown',60,70);emit('pointerup',60,70);check(events.pop()[3]===2&&!options,'one tap options clears');
emit('pointerdown',60,70,1,{button:2});emit('pointerup',60,70);check(events.pop()[3]===2,'mouse right click opens options');
emit('pointerdown',60,70);emit('pointercancel',60,70);emit('pointerup',60,70);check(events.length===0,'cancelled pointer does not click');
emit('wheel',60,70,1,{deltaY:20});assert.deepEqual(events.pop(),['wheel',100,100,1]);checks++;
emit('pointerdown',60,70);cancel();emit('pointerup',60,70);check(events.length===0,'hidden page clears gesture');
for(const [w,h,compact] of [[390,844,false],[844,390,true],[360,640,false],[740,360,true]]){const v=viewportSize(w,h,compact);check(v.width<=w&&v.height<=h&&Math.abs(v.width/v.height-765/503)<0.001,'phone viewport fits '+w+'x'+h);}
check(viewportSize(844,390,true).width>500,'landscape uses available space');
console.log('PASS '+checks+' mobile socket, touch and layout checks');