Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
405 lines
17 KiB
Python
405 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Media, alarms and visual regressions in genuine Gaia user applications."""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from marionette_client import Client
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--restart-check", action="store_true")
|
|
args = parser.parse_args()
|
|
results = {}
|
|
alarm_label = "Vulpes UI regression " + str(time.time_ns())
|
|
# A fresh filename avoids an earlier asynchronous MediaDB deletion being
|
|
# mistaken for this import. Keep the name for the restart assertions.
|
|
fixture_path = ROOT / "logs/apps-media-fixture.json"
|
|
media_fixture = (
|
|
json.loads(fixture_path.read_text())
|
|
if args.restart_check
|
|
else "vulpes-regression-" + str(time.time_ns()) + ".png"
|
|
)
|
|
if not args.restart_check:
|
|
fixture_path.write_text(json.dumps(media_fixture))
|
|
with Client(2857) as client:
|
|
engine = client.execute("return Services.appinfo.platformVersion;")
|
|
assert (
|
|
client.execute("return PathUtils.profileDir;").split("/")[-1].startswith("host-")
|
|
), "Test profile required"
|
|
client.execute(
|
|
"try{ChromeUtils.registerWindowActor('VulpesProbe',{child:{esModuleURI:'resource://vulpes/tests/ProbeChild.sys.mjs'},allFrames:true,safeForUntrustedWebProcess:true});}catch(_){}"
|
|
)
|
|
|
|
def ev(app, code, path="/"):
|
|
code = code.replace("vulpes-regression.png", media_fixture)
|
|
prefix = "http://" + app + ".localhost:8765" + path
|
|
script = """const done=arguments[arguments.length-1];const w=Services.wm.getMostRecentWindow('vulpes:host');
|
|
if(!w?.document.getElementById('system')){done({missing:true});return;}
|
|
const bc=(function find(b){if(b.currentWindowGlobal?.documentURI.spec.startsWith(PREFIX))return b;for(const c of b.children){const f=find(c);if(f)return f;}return null;})(w.document.getElementById('system').browsingContext);
|
|
if(!bc){done({missing:true});return;}bc.currentWindowGlobal.getActor('VulpesProbe').sendQuery('Evaluate',CODE).then(result=>done({result}),error=>done({error:String(error)}));""".replace(
|
|
"PREFIX", json.dumps(prefix)
|
|
).replace(
|
|
"CODE", json.dumps(code)
|
|
)
|
|
r = client.call(
|
|
"WebDriver:ExecuteAsyncScript",
|
|
{
|
|
"script": script,
|
|
"args": [],
|
|
"newSandbox": True,
|
|
"sandbox": "system",
|
|
"scriptTimeout": 15000,
|
|
},
|
|
)["value"]
|
|
if "error" in r:
|
|
raise AssertionError(r)
|
|
return r.get("result")
|
|
|
|
def wait(app, code, path="/"):
|
|
for _ in range(70):
|
|
r = ev(app, code, path)
|
|
if r:
|
|
return r
|
|
time.sleep(0.15)
|
|
raise AssertionError("Timeout: " + app + " " + code)
|
|
|
|
def check(name, condition):
|
|
results[name] = condition
|
|
print(name + ": " + json.dumps(condition, ensure_ascii=False), flush=True)
|
|
assert condition is True, (name, condition)
|
|
|
|
def launch(app, entry=None):
|
|
ev(
|
|
"system",
|
|
'await VulpesCompat.call("apps.launch",'
|
|
+ json.dumps(
|
|
{"manifestURL": f"http://{app}.localhost:8765/manifest.webapp", "entryPoint": entry}
|
|
)
|
|
+ ");return true;",
|
|
)
|
|
wait(app, "return document.readyState==='complete';")
|
|
if app == "clock":
|
|
wait(app, "return performance.getEntriesByName('fullyLoaded').length>0;")
|
|
wait(
|
|
"system",
|
|
"return [...document.querySelectorAll('.appWindow.active[transition-state=opened] iframe')].some(f=>f.src.startsWith('http://"
|
|
+ app
|
|
+ ".localhost:8765/'));",
|
|
)
|
|
time.sleep(0.5)
|
|
|
|
def focus():
|
|
client.execute(
|
|
"const w=Services.wm.getMostRecentWindow('vulpes:host');w.focus();w.document.getElementById('system').focus();return true;"
|
|
)
|
|
subprocess.run(
|
|
["xdotool", "search", "--name", "^Vulpes", "windowraise", "windowfocus"],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
)
|
|
|
|
def tap(app, selector, path="/"):
|
|
focus()
|
|
pos = ev(
|
|
app,
|
|
"const e="
|
|
+ selector
|
|
+ ';e.scrollIntoView({block:"nearest"});await new Promise(r=>setTimeout(r,150));const b=e.getBoundingClientRect();return {x:mozInnerScreenX+b.x+b.width/2,y:mozInnerScreenY+b.y+b.height/2};',
|
|
path,
|
|
)
|
|
subprocess.run(
|
|
["xdotool", "mousemove", str(round(pos["x"])), str(round(pos["y"])), "click", "1"],
|
|
check=True,
|
|
)
|
|
time.sleep(0.4)
|
|
|
|
def shot(name):
|
|
data = client.call("WebDriver:ExecuteAsyncScript", {
|
|
"script": """const done=arguments[arguments.length-1];
|
|
const w=Services.wm.getMostRecentWindow('vulpes:host'),b=w.document.getElementById('system');
|
|
b.browsingContext.currentWindowGlobal.drawSnapshot(new DOMRect(0,0,b.clientWidth,b.clientHeight),1,'white').then(bitmap=>{
|
|
const canvas=w.document.createElementNS('http://www.w3.org/1999/xhtml','canvas');
|
|
canvas.width=bitmap.width;canvas.height=bitmap.height;canvas.getContext('2d').drawImage(bitmap,0,0);
|
|
done(canvas.toDataURL());}).catch(e=>done({error:String(e)}));""",
|
|
"args": [], "newSandbox": True, "sandbox": "system", "scriptTimeout": 25000
|
|
})["value"]
|
|
if isinstance(data, str) and data.startswith("data:image/"):
|
|
(ROOT / "logs" / ("feedback-" + name + ".png")).write_bytes(base64.b64decode(data.split(",",1)[1]))
|
|
else:
|
|
print("Screenshot unavailable:", data, flush=True)
|
|
|
|
try:
|
|
wait("system", "return document.body?.getAttribute('ready-state')==='fullyLoaded';")
|
|
ev("system", "ScreenManager.turnScreenOn(true);return true;")
|
|
time.sleep(.5)
|
|
ev("system", "lockScreen.unlock(true);return true;")
|
|
time.sleep(.5)
|
|
wait("system", "return !document.getElementById('screen').classList.contains('locked');")
|
|
launch("gallery")
|
|
if args.restart_check:
|
|
check(
|
|
"photo-survives-restart",
|
|
ev(
|
|
"gallery",
|
|
"const f=await navigator.getDeviceStorage('pictures').get('vulpes-regression.png');return f.size>0 && f.type==='image/png';",
|
|
),
|
|
)
|
|
check(
|
|
"gallery-index-survives-restart",
|
|
wait("gallery", "return !!document.querySelector('.thumbnail');"),
|
|
)
|
|
launch("clock")
|
|
check(
|
|
"alarm-survives-restart",
|
|
ev(
|
|
"clock",
|
|
"return (await navigator.mozAlarms.getAll()).some(a=>a.data?.vulpesRegression==='persistence');",
|
|
),
|
|
)
|
|
raise SystemExit(0)
|
|
check(
|
|
"photo-import",
|
|
ev(
|
|
"gallery",
|
|
"""const c=document.createElement('canvas');c.width=128;c.height=96;const x=c.getContext('2d');x.fillStyle='#1686ae';x.fillRect(0,0,128,96);const blob=await new Promise(r=>c.toBlob(r,'image/png'));const s=navigator.getDeviceStorage('pictures');await s.delete('vulpes-regression.png');for(let n=0;n<100 && [...document.querySelectorAll('.thumbnailImage')].some(i=>i.dataset.filename==='/sdcard/vulpes-regression.png');n++)await new Promise(r=>setTimeout(r,50));await s.addNamed(blob,'vulpes-regression.png');const f=await s.get('vulpes-regression.png');return f.size===blob.size && f.type==='image/png' && Object.prototype.toString.call(f.lastModifiedDate)==='[object Date]';""",
|
|
),
|
|
)
|
|
check(
|
|
"gallery-thumbnail",
|
|
wait(
|
|
"gallery",
|
|
"return [...document.querySelectorAll('.thumbnailImage')].some(i=>i.dataset.filename==='/sdcard/vulpes-regression.png');",
|
|
),
|
|
)
|
|
time.sleep(0.6)
|
|
tap(
|
|
"gallery",
|
|
"[...document.querySelectorAll('.thumbnailImage')].find(i=>i.dataset.filename==='/sdcard/vulpes-regression.png').closest('.thumbnail')",
|
|
)
|
|
check(
|
|
"gallery-open-photo",
|
|
wait("gallery", "return document.body.classList.contains('fullscreenView');"),
|
|
)
|
|
shot("gallery-open")
|
|
check(
|
|
"media-path-traversal-denied",
|
|
ev(
|
|
"gallery",
|
|
"try{await navigator.getDeviceStorage('pictures').get('../vulpes-settings-v1.json');return false;}catch(e){return e.name==='SecurityError';}",
|
|
),
|
|
)
|
|
check(
|
|
"media-permissions-enforced",
|
|
ev(
|
|
"homescreen",
|
|
"try{await VulpesCompat.call('media.request',{type:'pictures',operation:'get',name:'vulpes-regression.png'});return false;}catch(e){return e.name==='PERMISSION_DENIED';}",
|
|
),
|
|
)
|
|
launch("music")
|
|
check(
|
|
"music-import",
|
|
ev(
|
|
"music",
|
|
"const s=navigator.getDeviceStorage('music');await s.delete('Vulpes-test.opus');const r=await fetch('http://clock.localhost:8765/shared/resources/media/alarms/ac_awake.opus');if(!r.ok)return false;await s.addNamed(await r.blob(),'Vulpes-test.opus');return true;",
|
|
),
|
|
)
|
|
tap("music", "document.querySelector('button[value=\"songs\"]')")
|
|
time.sleep(0.5)
|
|
if not ev("music", "return location.hash==='#/songs';"):
|
|
tap("music", "document.querySelector('button[value=\"songs\"]')")
|
|
check(
|
|
"music-songs-tab",
|
|
wait(
|
|
"music",
|
|
"return [...document.querySelectorAll('iframe')].some(f=>f.src.includes('/songs/'));",
|
|
),
|
|
)
|
|
check(
|
|
"music-song-indexed",
|
|
wait(
|
|
"music",
|
|
"return [...document.querySelectorAll('iframe')].some(f=>f.src.includes('/songs/') && f.contentDocument?.body?.innerText.includes('Vulpes-test'));",
|
|
),
|
|
)
|
|
check(
|
|
"music-toolbar-styled",
|
|
ev(
|
|
"music",
|
|
"const b=document.querySelector('button[value=songs]');return b.getBoundingClientRect().width>=80 && getComputedStyle(b).backgroundColor==='rgba(0, 0, 0, 0)';",
|
|
),
|
|
)
|
|
shot("music-songs")
|
|
tap("music", "document.querySelector('.gfl-item')", "/views/songs/")
|
|
check(
|
|
"music-plays-audio",
|
|
wait(
|
|
"music",
|
|
"const a=document.querySelector('audio');return !a.paused && a.currentTime>0;",
|
|
),
|
|
)
|
|
ev("music", "document.querySelector('audio').pause();return true;")
|
|
launch("clock")
|
|
tap("clock", "document.querySelector('#alarm-new')")
|
|
check(
|
|
"alarm-editor-opens",
|
|
wait(
|
|
"clock",
|
|
"return location.hash==='#alarm-edit-panel' && !!document.querySelector('#alarm-name');",
|
|
),
|
|
)
|
|
ev(
|
|
"clock",
|
|
"document.querySelector('#alarm-name').value="
|
|
+ json.dumps(alarm_label)
|
|
+ ";return true;",
|
|
)
|
|
tap("clock", "document.querySelector('#alarm-done')")
|
|
check(
|
|
"alarm-saved-through-ui",
|
|
wait(
|
|
"clock",
|
|
"const db=require('alarm_database');const alarm=(await db.getAll()).find(a=>a.label==="
|
|
+ json.dumps(alarm_label)
|
|
+ ");return !!alarm?.registeredAlarms.normal && document.body.innerText.includes("
|
|
+ json.dumps(alarm_label)
|
|
+ ");",
|
|
),
|
|
)
|
|
check(
|
|
"alarm-delivery",
|
|
ev(
|
|
"clock",
|
|
"""const list=await navigator.mozAlarms.getAll();const saved=(await require('alarm_database').getAll()).find(a=>a.label===ALARM_LABEL);const alarm=list.find(a=>a.data?.id===saved?.id && a.data?.type==='normal');if(!alarm)return false;await navigator.mozAlarms.add(new Date(Date.now()+700),'honorTimezone',alarm.data);return true;""".replace(
|
|
"ALARM_LABEL", json.dumps(alarm_label)
|
|
),
|
|
),
|
|
)
|
|
check(
|
|
"alarm-ring-view",
|
|
wait(
|
|
"clock",
|
|
"return !!document.querySelector('iframe[data-vulpes-attention]')?.contentDocument?.querySelector('#ring-button-stop');",
|
|
),
|
|
)
|
|
shot("alarm-ring")
|
|
check(
|
|
"alarm-plays-audio",
|
|
wait(
|
|
"clock",
|
|
"const w=document.querySelector('iframe[data-vulpes-attention]')?.contentWindow;return w?.ringView?.ringtonePlayer?._audio?.paused===false;",
|
|
),
|
|
)
|
|
tap("clock", "document.querySelector('#ring-button-stop')", "/onring.html")
|
|
check(
|
|
"alarm-stop-button",
|
|
wait("clock", "return !document.querySelector('iframe[data-vulpes-attention]');"),
|
|
)
|
|
check(
|
|
"alarm-no-uncaught-errors",
|
|
ev(
|
|
"clock",
|
|
"return !window.__vulpesDiagnostics.some(d=>d.kind==='error'||d.kind==='rejection');",
|
|
),
|
|
)
|
|
check(
|
|
"alarm-persistence-scheduled",
|
|
ev(
|
|
"clock",
|
|
"await navigator.mozAlarms.add(new Date(Date.now()+86400000),'honorTimezone',{vulpesRegression:'persistence'});return true;",
|
|
),
|
|
)
|
|
launch("settings")
|
|
check(
|
|
"settings-switch-type-size",
|
|
ev(
|
|
"settings",
|
|
"return [...document.querySelectorAll('#root gaia-switch label')].filter(e=>e.getBoundingClientRect().height).every(e=>parseFloat(getComputedStyle(e).fontSize)>=19);",
|
|
),
|
|
)
|
|
shot("settings-switches")
|
|
launch("email")
|
|
check(
|
|
"email-field-appearance",
|
|
wait(
|
|
"email",
|
|
"const e=document.querySelector('input[type=email]');return e && getComputedStyle(e).appearance==='none';",
|
|
),
|
|
)
|
|
for app in ["costcontrol", "fm"]:
|
|
launch(app)
|
|
check(
|
|
app + "-desktop-capability",
|
|
ev(
|
|
app,
|
|
"return !document.querySelector('.vulpes-capability') && document.body.innerText.length>0 && !window.__vulpesDiagnostics.some(d=>d.kind==='error'||d.kind==='rejection');",
|
|
),
|
|
)
|
|
focus()
|
|
subprocess.run(["xdotool", "mousemove", "240", "820", "mousedown", "1"], check=True)
|
|
time.sleep(0.85)
|
|
subprocess.run(["xdotool", "mouseup", "1"], check=True)
|
|
check(
|
|
"long-home-task-manager",
|
|
wait(
|
|
"system",
|
|
"return document.querySelector('#screen').classList.contains('cards-view');",
|
|
),
|
|
)
|
|
shot("task-manager")
|
|
before = ev(
|
|
"system", "return [...document.querySelectorAll('.appWindow iframe')].map(f=>f.src);"
|
|
)
|
|
tap(
|
|
"system",
|
|
"[...document.querySelectorAll('.close-button')].find(e=>{const r=e.getBoundingClientRect();return r.x>=0&&r.right<480;})",
|
|
)
|
|
check(
|
|
"task-manager-closes-app",
|
|
wait(
|
|
"system",
|
|
"const before="
|
|
+ json.dumps(before)
|
|
+ ";const after=[...document.querySelectorAll('.appWindow iframe')].map(f=>f.src);return before.some(url=>!after.includes(url));",
|
|
),
|
|
)
|
|
ev("system", "dispatchEvent(new CustomEvent('home'));return true;")
|
|
time.sleep(0.5)
|
|
check(
|
|
"home-transparent-chrome",
|
|
ev(
|
|
"system",
|
|
"return getComputedStyle(document.querySelector('#homescreen > .chrome')).backgroundColor==='rgba(0, 0, 0, 0)';",
|
|
),
|
|
)
|
|
check(
|
|
"browser-localized-name",
|
|
ev(
|
|
"system",
|
|
"const a=(await navigator.mozApps.mgmt.getAll()).find(a=>a.origin.includes('search.localhost'));return a.manifest.locales.fr.name==='Navigateur' && Object.values(a.manifest.icons).every(i=>i.includes('vulpes-browser.png'));",
|
|
),
|
|
)
|
|
shot("home-transparent")
|
|
except Exception:
|
|
shot("apps-failure")
|
|
print(
|
|
"FAILURE CLOCK",
|
|
json.dumps(
|
|
ev(
|
|
"clock",
|
|
"return {body:document.body.innerText,hash:location.hash,errors:window.__vulpesDiagnostics,button:document.querySelector('#alarm-new')?.getBoundingClientRect().toJSON()};",
|
|
)
|
|
),
|
|
flush=True,
|
|
)
|
|
raise
|
|
finally:
|
|
(
|
|
ROOT
|
|
/ "logs"
|
|
/ ("apps-restart-results.json" if args.restart_check else "apps-results.json")
|
|
).write_text(json.dumps(results, indent=2) + "\n")
|