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.
This commit is contained in:
OverSu 2026-10-14 13:13:46 +02:00
commit 200d1fb235
43 changed files with 2271 additions and 65 deletions

View file

@ -0,0 +1,40 @@
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');
});