VulpesOS/tests/android-input-bridge.test.mjs
OverSu 200d1fb235 Release Android Preview 21 and bilingual changelog
Add permission-based reading of Android contacts, SMS and call history. Include shared Gaia fixes and document device validation limits. Pixel image qualification remains pending.
2026-10-14 13:13:46 +02:00

104 lines
5.1 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('../android/extension/content.js',import.meta.url),'utf8');
async function setup(enabled=true,{defer=false,activeElement}={}) {
const sent=[],events=new Map(),timers=[];let receive,disconnect;
const page={navigator:{},dispatchEvent(){}};
const port={postMessage:m=>sent.push(m),onMessage:{addListener:fn=>receive=fn},onDisconnect:{addListener:fn=>disconnect=fn}};
class Editor {
constructor(){this.editing=false;}
focus(e){this.inputElement=e;}
state(){return {inputMode:this.inputElement.getAttribute('inputmode'),text:'',inputType:'text'};}
edit(){return this.state();}
}
const context={InputEditor:Editor,browser:{runtime:{connect:()=>port}},console,
location:{port:'18765',hostname:'sms.localhost'},document:{hidden:false,activeElement,hasFocus:()=>true},
window:{wrappedJSObject:page,Promise,Error,Event,CustomEvent:class extends Event{constructor(name,p){super(name);this.detail=p.detail;}}},
cloneInto:x=>x,exportFunction:(fn,obj,p)=>{obj[p.defineAs]=fn;},
addEventListener:(name,fn)=>events.set(name,fn),setTimeout:fn=>timers.push(fn),clearTimeout(){}};
vm.runInNewContext(source,context);
if(!defer) receive({id:sent[0].id,reply:{result:enabled}});
await new Promise(resolve=>setImmediate(resolve));
return {sent,events,page,receive,context,timers,disconnect};
}
function field(mode='numeric') {
const attrs=new Map([['inputmode',mode]]);
return {localName:'input',type:'text',classList:{contains:()=>false},
getAttribute:key=>attrs.get(key)??null,setAttribute:(k,v)=>attrs.set(k,v),removeAttribute:k=>attrs.delete(k)};
}
test('Android bridge keeps the intended keypad mode after editing, while suppressing the Android IME',async()=>{
const s=await setup(),e=field();
s.events.get('focusin')({target:e});
assert.equal(e.getAttribute('inputmode'),'none');
assert.equal(s.sent.at(-1).inputFocus.inputMode,'numeric');
s.receive({edit:{id:3,params:{operation:'key',key:1}}});
assert.equal(s.sent.at(-1).editReply,3);
assert.equal(s.sent.at(-1).result.inputMode,'numeric');
s.receive({event:{type:'input-mode',data:{enabled:false}}});
assert.equal(e.getAttribute('inputmode'),'numeric');
assert.equal(s.page.VulpesNativeInput,false);
});
test('Android keyboard remains the default and hidden Gaia restores the editor mode',async()=>{
const s=await setup(false),e=field();
s.events.get('focusin')({target:e});
assert.equal(e.getAttribute('inputmode'),'numeric');
assert.equal(s.sent.length,1);
s.receive({event:{type:'input-mode',data:{enabled:true}}});
s.events.get('focusin')({target:e});
s.context.document.hidden=true;s.events.get('visibilitychange')();
assert.equal(e.getAttribute('inputmode'),'numeric');
assert.equal(s.sent.at(-1).inputFocus,null);
});
test('a field focused before the preference arrives activates Gaia without another tap',async()=>{
const e=field(),s=await setup(true,{defer:true,activeElement:e});
s.events.get('focusin')({target:e});
assert.equal(s.sent.length,1);
s.receive({id:s.sent[0].id,reply:{result:true}});
await new Promise(resolve=>setImmediate(resolve));
assert.equal(e.getAttribute('inputmode'),'none');
assert.equal(s.sent.at(-1).inputFocus.inputMode,'numeric');
});
test('an old preference reply cannot undo a newer keyboard toggle',async()=>{
const e=field(),s=await setup(false,{defer:true,activeElement:e});
s.receive({event:{type:'input-mode',data:{enabled:true}}});
s.receive({id:s.sent[0].id,reply:{result:false}});
await new Promise(resolve=>setImmediate(resolve));
assert.equal(s.page.VulpesNativeInput,true);
assert.equal(e.getAttribute('inputmode'),'none');
});
test('returning to an app restores its focused Gaia editor',async()=>{
const e=field(),s=await setup(true,{activeElement:e});
s.context.document.hidden=true;s.events.get('visibilitychange')();
assert.equal(s.sent.at(-1).inputFocus,null);
s.context.document.hidden=false;s.events.get('visibilitychange')();
assert.equal(s.sent.at(-1).inputFocus.inputMode,'numeric');
assert.equal(e.getAttribute('inputmode'),'none');
});
test('bridge reconnection rereads the preference and restores the active editor',async()=>{
const e=field(),s=await setup(true,{activeElement:e});
s.disconnect();
assert.equal(e.getAttribute('inputmode'),'numeric');
assert.equal(s.page.VulpesNativeInput,false);
s.timers.at(-1)();
const request=s.sent.at(-1);
assert.equal(request.request.method,'input.config');
s.receive({id:request.id,reply:{result:true}});
await new Promise(resolve=>setImmediate(resolve));
assert.equal(e.getAttribute('inputmode'),'none');
assert.equal(s.sent.at(-1).inputFocus.inputMode,'numeric');
});
test('a background app cannot reclaim the keyboard when Android resumes',async()=>{
const e=field(),s=await setup(true,{activeElement:e});
s.context.document.hidden=true;s.events.get('visibilitychange')();
s.context.document.hidden=false;s.context.document.hasFocus=()=>false;
s.events.get('visibilitychange')();
assert.equal(s.sent.at(-1).inputFocus,null);
assert.equal(e.getAttribute('inputmode'),'numeric');
});