Update shared Gaia workflows and Sargo development tooling

Fix camera switching and Contacts/SMS activity routing across shared hosts.
Integrate Marketplace installation and native standalone recovery tools,
Bluetooth runtime packages and trusted certificates.

Update English/French READMEs and the shared changelog with tested scope
and remaining radio/audio/Bluetooth limitations. Preview17 remains the
existing APK; no new native image is released.

Validation: shared JavaScript and native Python tests; Android packaging
and payload parity from an isolated publication checkout.
This commit is contained in:
OverSu 2026-10-08 14:27:17 +02:00
commit 0041cd9660
101 changed files with 3883 additions and 335 deletions

183
tools/test-contacts.py Normal file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Exercise Gaia Contacts and its SMS/dialer activities in the desktop test profile.
Creates one temporary contact; never sends a message or starts a call.
"""
import importlib.util
import json
import time
import sys
from pathlib import Path
from marionette_client import Client
spec = importlib.util.spec_from_file_location('probe', Path(__file__).with_name('probe-desktop.py'))
probe = importlib.util.module_from_spec(spec)
spec.loader.exec_module(probe)
results = {}
contact_id = None
photo_name = None
name = 'Vulpes QA ' + str(time.time_ns())
with Client(2857) as client:
client.socket.settimeout(90)
probe.register(client)
def ev(app, code):
result = probe.evaluate(client, 'communications' if app == 'dialer' else app, code, '/dialer/' if app == 'dialer' else '/contacts/' if app == 'communications' else '/')
if 'error' in result:
raise AssertionError(result)
return result.get('value')
def wait(app, code):
for _ in range(100):
try:
value = ev(app, code)
except AssertionError as error:
if 'Actor' not in str(error) or 'destroyed' not in str(error):
raise
value = None
if value:
return value
time.sleep(.15)
raise TimeoutError(app + ': ' + code)
def click(app, selector):
print("click", app, selector, file=sys.stderr, flush=True)
point = wait(app, 'for(const e of document.querySelectorAll(' + json.dumps(selector) + ')) {'
'const r=e.getBoundingClientRect(),x=r.x+r.width/2,y=r.y+r.height/2;'
'if(x>0&&y>0&&x<innerWidth&&y<innerHeight&&e.contains(document.elementFromPoint(x,y)))return {x,y};}return false;')
script = '''const done=arguments[arguments.length-1];
const root=Services.wm.getMostRecentWindow('vulpes:host').document.getElementById('system').browsingContext;
function find(b){if(b.currentWindowGlobal?.documentURI.spec.startsWith(PREFIX))return b;
for(const c of b.children){const found=find(c);if(found)return found;}}
find(root).currentWindowGlobal.getActor('VulpesProbe').sendQuery('MouseClick',POINT)
.then(()=>done(true),e=>done({error:String(e)}));'''.replace('PREFIX', json.dumps('http://' + app + '.localhost:8765/')).replace('POINT', json.dumps(point))
clicked = client.call('WebDriver:ExecuteAsyncScript', {'script': script, 'args': [],
'newSandbox': True, 'sandbox': 'system', 'scriptTimeout': 10000})['value']
assert clicked is True, clicked
def field(selector, value):
wait('communications', 'return !!document.querySelector(' + json.dumps(selector) + ');')
ev('communications', 'const e=document.querySelector(' + json.dumps(selector) + ');'
'e.value=' + json.dumps(value) + ';e.dispatchEvent(new Event("input",{bubbles:true}));return true;')
def contact():
return ev('communications', '''const r=navigator.mozContacts.find({filterBy:['givenName'],
filterOp:'equals',filterValue:NAME});return await new Promise((ok,no)=>{
r.onsuccess=()=>ok(r.result[0]||null);r.onerror=()=>no(r.error);});'''.replace('NAME', json.dumps(name)))
try:
wait('system', 'return document.body?.getAttribute("ready-state")==="fullyLoaded";')
ev('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;")
wait('communications', 'return document.readyState==="complete"&&!document.hidden;')
ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://gallery.localhost:8765/manifest.webapp'});return true;")
wait('gallery', 'return document.readyState==="complete"&&!document.hidden;')
fixture='vulpes-contacts-qa-'+str(time.time_ns())+'.jpg'
photo_name=ev('gallery', 'const c=document.createElement("canvas");c.width=c.height=400;'
'const ctx=c.getContext("2d");ctx.fillStyle="#2372ae";ctx.fillRect(0,0,400,400);'
'const blob=await new Promise(r=>c.toBlob(r,"image/jpeg"));'
'return await VulpesCompat.call("media.request",{operation:"add",type:"pictures",name:'+json.dumps(fixture)+',blob});')
ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;")
token = str(time.time_ns())
ev('communications', 'setTimeout(()=>location.href=location.origin+"/contacts/index.html?qa='+token+'",0);return true;')
wait('communications', 'return location.search==="?qa='+token+'" && document.readyState==="complete";')
wait('communications', 'return typeof Loader!=="undefined" && !!window.Contacts && MainNavigation.currentView()==="view-contacts-list";')
click('communications', '#add-contact-button')
wait('communications', 'return window.MainNavigation?.currentView()==="view-contact-form" && !!document.querySelector("#number_0");')
field('#givenName', name)
field('#familyName', 'Élodie Test')
field('#number_0', '5550199')
field('#email_0', 'qa@example.invalid')
click('communications', '#save-button')
for _ in range(80):
row = contact()
if row:
contact_id = row['id']
break
time.sleep(.15)
assert contact_id
results['create'] = row['tel'][0]['value'] == '5550199'
item = '[data-uuid=' + json.dumps(contact_id) + ']'
click('communications', '#search-start')
wait('communications', "return MainNavigation.currentView()==='search-view' && !!document.querySelector('#search-contact');")
field('#search-contact', 'elodie')
results['search_without_accent'] = bool(wait('communications', 'return !!document.querySelector(' + json.dumps('#search-list [data-uuid="' + contact_id + '"]') + ');'))
field('#search-contact', name + ' absent')
results['search_no_results'] = bool(wait('communications', "return !document.querySelector('#no-result').classList.contains('hide');"))
click('communications', '#cancel-search')
wait('communications', "return MainNavigation.currentView()==='view-contacts-list';")
click('communications', item)
click('communications', '#edit-contact-button')
wait('communications', 'return MainNavigation.currentView()==="view-contact-form";')
click('communications', '#photo-button')
wait('gallery', 'return !document.hidden && typeof picking!=="undefined" && picking;')
thumb='.thumbnailImage[data-filename='+json.dumps(photo_name)+']'
click('gallery',thumb)
wait('gallery','return !document.querySelector("#crop-done-button").disabled;')
click('gallery','#crop-done-button')
wait('communications', 'return !document.hidden && getComputedStyle(document.querySelector("#thumbnail-photo")).backgroundImage!=="none";')
field('#familyName', 'Élodie Modifiée')
click('communications', '#save-button')
results['edit_detail_refresh'] = bool(wait('communications', 'return document.querySelector("#contact-name-title")?.textContent.includes("Élodie Modifiée");'))
assert contact()['familyName'] == ['Élodie Modifiée']
results['contact_photo']=bool(ev('communications', 'const r=navigator.mozContacts.find({filterBy:["id"],filterOp:"equals",filterValue:'+json.dumps(contact_id)+'});const c=await new Promise(ok=>{r.onsuccess=()=>ok(r.result[0]);});return c.photo?.[0] instanceof Blob && c.photo[0].size>0;'))
assert results['contact_photo']
ev('communications', 'document.querySelector("#toggle-favorite").scrollIntoView({block:"center"});return true;')
click('communications', '#toggle-favorite')
results['favorite'] = bool(wait('communications', 'const r=navigator.mozContacts.find({filterBy:["id"],filterOp:"equals",filterValue:'+json.dumps(contact_id)+'});const c=await new Promise(ok=>{r.onsuccess=()=>ok(r.result[0]);});return c.category?.includes("favorite");'))
ev('communications', 'document.querySelector("#send-sms-button-0").scrollIntoView({block:"center"});return true;')
click('communications', '#send-sms-button-0')
results['sms_recipient'] = bool(wait('sms', 'return !document.hidden&&window.ConversationView?.recipients?.numbers.includes("5550199");'))
click('sms', '#messages-contact-pick-button')
wait('communications', 'return !document.hidden&&ActivityHandler.currentlyHandling;')
click('communications', item)
wait('communications', 'return !!document.querySelector("[data-l10n-id=pick_destination]");')
click('communications', '[data-l10n-id=pick_destination]')
wait('communications', 'return !ActivityHandler.currentlyHandling;')
results['picker_return'] = bool(wait('sms', 'return !document.hidden&&ConversationView.recipients.numbers.includes("5550199");'))
# This activity only prefills the original dialer; do not press Call.
ev('sms', 'window.__dialQA=new MozActivity({name:"dial",data:{type:"webtelephony/number",number:"5550199"}});return true;')
results['dialer_prefill'] = bool(wait('dialer', 'return !document.hidden&&document.querySelector("#phone-number-view")?.value==="5550199";'))
ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;")
wait('communications', 'return !document.hidden;')
click('communications', item)
click('communications', '#edit-contact-button')
wait('communications', 'return MainNavigation.currentView()==="view-contact-form";')
ev('communications', 'document.querySelector("#delete-contact").scrollIntoView({block:"center"});return true;')
click('communications', '#delete-contact')
click('communications', '#confirmation-message button[data-l10n-id=cancel]')
results['delete_cancel'] = contact() is not None
wait('communications', 'return document.querySelector("#confirmation-message").classList.contains("hide");')
click('communications', '#delete-contact')
click('communications', '#confirmation-message button[data-l10n-id=delete]')
for _ in range(100):
if contact() is None:
results['delete_confirm'] = True
results['temporary_contact_removed'] = True
contact_id = None
break
time.sleep(.15)
assert results.get('delete_confirm')
results['passed'] = all(results.values())
finally:
try:
if contact_id:
# Return to the address book before removing only our temporary entry.
ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;")
ev('communications', 'const r=navigator.mozContacts.remove(' + json.dumps(contact_id) + ');await new Promise((ok,no)=>{r.onsuccess=ok;r.onerror=()=>no(r.error);});return true;')
results['temporary_contact_removed'] = contact() is None
except Exception as error:
results['cleanup_error'] = str(error)
results['temporary_contact_id'] = contact_id
finally:
if photo_name:
try:
ev('gallery','await VulpesCompat.call("media.request",{operation:"delete",type:"pictures",name:'+json.dumps(photo_name)+'});return true;')
results['temporary_photo_removed']=True
except Exception as error:
results['photo_cleanup_error']=str(error)
results['temporary_photo_name']=photo_name
print(json.dumps(results, indent=2, ensure_ascii=False))
if not results.get('passed') or results.get('cleanup_error') or results.get('photo_cleanup_error'):
raise SystemExit('Contacts qualification failed; see the report above.')