Emberveil source

tests/mobile-chat.mjs

import assert from 'node:assert/strict';import {readFile} from 'node:fs/promises';
const code=await readFile(new URL('../mobile/public/chat.js',import.meta.url));
const {bindChat}=await import('data:text/javascript;base64,'+code.toString('base64'));
globalThis.innerWidth=390;globalThis.innerHeight=780;
let checks=0;function check(v,m){assert.ok(v,m);checks++;}
function element(){return {hidden:true,value:'',style:{},offsetHeight:54,events:{},attrs:{},focuses:0,blurs:0,focus(){this.focuses++;},blur(){this.blurs++;},setAttribute(k,v){this.attrs[k]=v;},addEventListener(k,v){this.events[k]=v;}};}
const form=element(),input=element(),send=element(),close=element(),toggle=element();let online=false,writes=[],layouts=0,message='',release,fail=false;
const view={height:780,offsetTop:0},viewport={getBoundingClientRect:()=>({left:7,bottom:650,width:376})};
const chat=bindChat({form,input,send,close,toggle,viewport,view,connected:()=>online,layout:()=>layouts++,status:s=>message=s,write:text=>{writes.push(text);if(fail)throw Error('offline');return new Promise(resolve=>release=resolve);}});
chat.open();check(form.hidden,'cannot chat before login');online=true;chat.open();check(!form.hidden&&toggle.attrs['aria-expanded']==='true','opens over game');check(input.focuses===1,'keyboard opens within gesture');check(layouts===1,'layout refreshed');
view.height=350;chat.position();check(parseFloat(form.style.top)+54<=344,'composer above keyboard');check(form.style.width==='376px','stays within game width');
input.value='Hello Emberveil';let pending=form.onsubmit({preventDefault(){}});check(writes[0]==='Hello Emberveil','chat reaches Java bridge');check(send.disabled,'duplicate send blocked');check(input.focuses===2&&input.blurs===0,'send retains focus before async work');await form.onsubmit({preventDefault(){}});check(writes.length===1,'duplicate prevented');release();await pending;check(input.value===''&&!send.disabled,'successful draft cleared and send enabled');check(!form.hidden&&input.blurs===0,'chat remains open after send');
input.value='Try again';fail=true;await form.onsubmit({preventDefault(){}});check(input.value==='Try again'&&message.includes('still here'),'failed send preserves draft');check(!send.disabled,'retry enabled');fail=false;
input.value='First';pending=form.onsubmit({preventDefault(){}});input.value='Second';input.events.input();release();await pending;check(input.value==='Second','new typing during send preserved');
input.value='🙂';await form.onsubmit({preventDefault(){}});check(input.value==='🙂'&&message.includes('punctuation'),'unsupported text preserved with explanation');input.value='  ';const before=writes.length;await form.onsubmit({preventDefault(){}});check(writes.length===before,'blank not sent');
let prevented=false;send.events.pointerdown({preventDefault(){prevented=true;}});check(prevented,'send does not steal keyboard focus');input.value='saved draft';close.onclick();check(form.hidden&&input.blurs===1,'explicit close hides keyboard');check(input.value==='saved draft','closing preserves draft');toggle.onclick();check(!form.hidden&&input.value==='saved draft','reopening restores draft');
for(const [width,height]of[[390,300],[844,230],[320,200]]){globalThis.innerWidth=width;view.height=height;chat.position();check(parseFloat(form.style.left)>=0&&parseFloat(form.style.top)>=0,'portrait landscape narrow layout bounded');}
console.log('PASS',checks,'mobile chat focus, draft, retry, duplicate-send and keyboard-position checks');