VulpesOS/tests/keyboard-startup.test.mjs
2026-10-22 20:42:26 +02:00

87 lines
4.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
const source=readFileSync(new URL('../host/gaia-input.js',import.meta.url),'utf8');
function keyboard() {
const listeners=new Map();let reply;
const parent={postMessage(){}};
const context={VulpesNativeInput:true,location:{hostname:'keyboard.localhost'},
VulpesCompat:{call:()=>new Promise(resolve=>reply=resolve)},
navigator:{},document:{readyState:'loading'},parent,console,Event,EventTarget,
CustomEvent:class extends Event {constructor(name,options){super(name);this.detail=options.detail;}},
addEventListener(name,fn){listeners.set(name,fn);},dispatchEvent(){}};
context.window=context;
vm.runInNewContext(source,context);
return {
ime:context.navigator.mozInputMethod,
state:value=>listeners.get('vulpes-service-event')({detail:{type:'input-focus',data:value}}),
reply:value=>reply(value),
viewport:()=>listeners.get('message')({source:parent,origin:'http://system.localhost:8765',
data:{type:'vulpes-keyboard-viewport',width:360,height:740}})
};
}
test('an old empty snapshot cannot clear a newly focused keyboard',async()=>{
const k=keyboard();k.viewport();k.state({id:2,text:'current'});k.reply(null);
await new Promise(resolve=>setImmediate(resolve));
assert.equal(k.ime.inputcontext.text,'current');
});
test('an old active snapshot cannot reopen a dismissed keyboard',async()=>{
const k=keyboard();k.viewport();k.state(null);k.reply({id:1,text:'old'});
await new Promise(resolve=>setImmediate(resolve));
assert.equal(k.ime.inputcontext,null);
});
test('focus received before viewport measurement survives a stale initial reply',async()=>{
const k=keyboard();k.state({id:3,text:'pending'});k.reply(null);
await new Promise(resolve=>setImmediate(resolve));
k.viewport();
assert.equal(k.ime.inputcontext.text,'pending');
});
function systemKeyboard({legacy=true}={}) {
const events=new EventTarget(), intervals=[], calls=[], states=new Map();
const frame={style:{},hidden:true,contentWindow:{postMessage(){}},addEventListener(){}};
const context={VulpesNativeInput:true,location:{hostname:'system.localhost'},
VulpesCompat:{call:(method,params)=>{calls.push({method,params});return Promise.resolve();}},
document:{documentElement:{lang:'fr'},createElement:()=>frame,body:{append(){}}},navigator:{language:'fr'},
inputWindowManager:legacy?{}:undefined,Service:{registerState:(name,provider)=>states.set(name,provider),register:(name,provider)=>states.set(name,provider)},
console,Event,EventTarget,CustomEvent:class extends Event {constructor(name,options={}){super(name);this.detail=options.detail;}},
setInterval:fn=>{intervals.push(fn);return intervals.length;},clearInterval(){},
addEventListener:events.addEventListener.bind(events),dispatchEvent:events.dispatchEvent.bind(events)};
context.window=context;vm.runInNewContext(source,context);intervals.forEach(fn=>fn());
const focus=value=>events.dispatchEvent(new context.CustomEvent('vulpes-service-event',{detail:{type:'input-focus',data:value}}));
return {events,context,frame,calls,focus,states};
}
test('Gaia hide commands target the host keyboard',async()=>{
const s=systemKeyboard();
await s.context.inputWindowManager.hideInputWindow();
await s.context.inputWindowManager.hideInputWindowImmediately();
assert.equal(s.calls.length,2);
assert.ok(s.calls.every(c=>c.method==='input.request' && c.params.operation==='hide'));
});
test('keyboardhidden follows layout completion so app switching can resume',async()=>{
const s=systemKeyboard(), seen=[];let finish;
s.events.addEventListener('keyboardhide',e=>{seen.push('hide');e.detail.waitUntil(new Promise(r=>finish=r));});
s.events.addEventListener('keyboardhidden',()=>seen.push('hidden'));
s.focus({id:1});s.focus(null);
assert.equal(s.frame.hidden,true);assert.deepEqual(seen,['hide']);
finish();await new Promise(r=>setImmediate(r));
assert.deepEqual(seen,['hide','hidden']);
});
test('refocus cancels stale keyboardhidden completion',async()=>{
const s=systemKeyboard();let finish,hidden=0;
s.events.addEventListener('keyboardhide',e=>e.detail.waitUntil(new Promise(r=>finish=r)));
s.events.addEventListener('keyboardhidden',()=>hidden++);
s.focus({id:1});s.focus(null);s.focus({id:2});finish();
await new Promise(r=>setImmediate(r));
assert.equal(hidden,0);assert.equal(s.frame.hidden,false);
});
test('standalone host registers hide services without the legacy KeyboardManager',async()=>{
const s=systemKeyboard({legacy:false});
for(const name of ['hideInputWindow','hideInputWindowImmediately'])
await s.states.get(name)[name]();
assert.equal(s.calls.length,2);
s.focus({id:1});assert.equal(s.states.get('getHeight').getHeight(),260);
});