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

70 lines
3.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {Input} from '../services/input.mjs';
function setup() {
const events=[];
const input=new Input({identity:actor=>actor.app,broadcast:(...args)=>events.push(args)});
const keyboard={app:{id:'keyboard'}}, field={app:{id:'sms'}};
return {input,events,keyboard,field};
}
test('keyboard cannot edit a stale field or another app issue keyboard commands',async()=>{
const {input,keyboard,field}=setup();
input.changed(field,{text:'',id:0});
await assert.rejects(input.request(field,{operation:'state'}),/PERMISSION_DENIED/);
await assert.rejects(input.request(keyboard,{operation:'key',id:0}),/INPUT_CONTEXT_EXPIRED/);
assert.equal((await input.request(keyboard,{operation:'state'})).id,1);
});
for(const action of ['hide','change']) test(`pending edit cannot update state after ${action}`,async()=>{
const {input,keyboard,field}=setup();let reply;
field.sendQuery=()=>new Promise(resolve=>{reply=resolve;});
input.changed(field,{text:'first'});
const pending=input.request(keyboard,{operation:'key',id:1});
if(action==='hide') input.hide(); else input.changed(field,{text:'second'});
reply({text:'first edited'});
await assert.rejects(pending,/INPUT_CONTEXT_EXPIRED/);
assert.equal(input.focus?.state.text,action==='hide'?undefined:'second');
});
test('current edit updates the focused field; keyboard focus does not replace it',async()=>{
const {input,keyboard,field}=setup();
field.sendQuery=async()=>({text:'123',selectionStart:3,selectionEnd:3});
input.changed(field,{text:''});input.changed(keyboard,{text:'unrelated'});
assert.equal((await input.request(keyboard,{operation:'key',id:1})).text,'123');
});
test('a composer button retains input and other buttons dismiss only after click',async()=>{
const {readFile}=await import('node:fs/promises');
const {runInNewContext}=await import('node:vm');
const source=await readFile(new URL('../host/actors/VulpesChild.sys.mjs',import.meta.url),'utf8');
const events=[];
const Actor=runInNewContext(source.replace(/^import .*;\n/gm, '').replace('export class','class')+';VulpesChild',{
JSWindowActorChild:class{},Services:{cpmm:{sharedData:{get:()=>true}}}
});
const actor=new Actor();actor.sendAsyncMessage=(...args)=>events.push(args);
const button={localName:'button',closest:()=>null};
actor.handleEvent({type:'pointerdown',target:button});assert.equal(events.length,0);
actor.handleEvent({type:'click',target:button});assert.equal(events.length,1);assert.equal(events[0][1],null);
button.closest=()=>button;
actor.handleEvent({type:'pointerdown',target:button});actor.handleEvent({type:'click',target:button});
assert.equal(events.length,1,'send keeps the composer focused');
});
test('physical keypad suppresses the virtual IME without affecting touch hosts',()=>{
const {input,events,field}=setup();input.runtime.hardwareKeyboard=true;
input.changed(field,{text:'private text'});
assert.equal(input.focus,null);assert.equal(events.length,0);
input.runtime.hardwareKeyboard=false;input.changed(field,{text:''});
assert.equal(events[0][0],'input-focus');
});
test('an external input uses its visible owner, without gaining keyboard permissions',async()=>{
const {input,keyboard}=setup();const web={};let visible=true;
input.runtime.inputIdentity=actor=>actor===web && visible?{id:'marketplace'}:actor.app;
web.sendQuery=async()=>({text:'test'});
input.changed(web,{text:''});
assert.equal((await input.request(keyboard,{operation:'key',id:1})).text,'test');
await assert.rejects(input.request(web,{operation:'state'}),/PERMISSION_DENIED/);
visible=false;
await assert.rejects(input.request(keyboard,{operation:'key',id:1}),/INPUT_CONTEXT_EXPIRED/);
assert.equal(input.focus,null);
input.changed(web,{text:'hidden'});assert.equal(input.focus,null);
});