VulpesOS/tools/test-legacy-apps.py
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

253 lines
11 KiB
Python

#!/usr/bin/env python3
"""Observe original archived apps in real Gaia; failures stay visible in the report."""
import argparse
import json
import subprocess
import time
from pathlib import Path
from marionette_client import Client
ROOT = Path(__file__).resolve().parents[1]
p = argparse.ArgumentParser()
p.add_argument("--restart-check", action="store_true")
args = p.parse_args()
results = {"engine": None, "restart": args.restart_check, "apps": {}}
with Client(2857) as client:
profile = client.execute("return PathUtils.profileDir;")
assert "/host-legacy-" in profile, profile
results["engine"] = client.execute("return Services.appinfo.platformVersion;")
client.execute(
"try{ChromeUtils.registerWindowActor('VulpesProbe',{child:{esModuleURI:'resource://vulpes/tests/ProbeChild.sys.mjs'},allFrames:true,safeForUntrustedWebProcess:true});}catch(_){}"
)
def ev(app, code):
script = """const done=arguments[arguments.length-1];const w=Services.wm.getMostRecentWindow('vulpes:host');
const b=w?.document.getElementById('system');if(!b){done(null);return;}
const bc=(function f(b){if(b.currentWindowGlobal?.documentURI.spec.startsWith(PREFIX))return b;for(const c of b.children){const found=f(c);if(found)return found;}return null;})(b.browsingContext);
if(!bc){done(null);return;}bc.currentWindowGlobal.getActor('VulpesProbe').sendQuery('Evaluate',CODE).then(v=>done({value:v}),e=>done({error:String(e)}));""".replace(
"PREFIX", json.dumps("http://" + app + ".localhost:8765/")
).replace(
"CODE", json.dumps(code)
)
r = client.call(
"WebDriver:ExecuteAsyncScript",
{
"script": script,
"args": [],
"newSandbox": True,
"sandbox": "system",
"scriptTimeout": 15000,
},
)["value"]
if r and "error" in r:
raise RuntimeError(r["error"])
return r.get("value") if r else None
def wait(app, code, timeout=30):
end = time.monotonic() + timeout
while time.monotonic() < end:
r = ev(app, code)
if r:
return r
time.sleep(0.2)
raise RuntimeError("Timeout: " + app + " " + code)
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 point(app, selector):
return ev(
app,
"const e=document.querySelector("
+ json.dumps(selector)
+ ');e.scrollIntoView({block:"center"});const b=e.getBoundingClientRect();return {x:mozInnerScreenX+b.x+b.width/2,y:mozInnerScreenY+b.y+b.height/2};',
)
def tap(app, selector):
pos = point(app, selector)
focus()
subprocess.run(
["xdotool", "mousemove", str(round(pos["x"])), str(round(pos["y"])), "click", "1"],
check=True,
)
time.sleep(0.3)
def shot(app):
focus()
subprocess.run(
[
"import",
"-window",
"root",
str(
ROOT
/ "logs"
/ ("archive-" + app + ("-restart" if args.restart_check else "") + ".png")
),
],
check=True,
)
wait("system", "return document.body?.getAttribute('ready-state')==='fullyLoaded';", 60)
for app in json.loads((ROOT / "tests/legacy-apps.json").read_text()):
name = app["id"]
r = {}
results["apps"][name] = r
try:
ev(
"system",
'await VulpesCompat.call("apps.launch",'
+ json.dumps({"manifestURL": "http://" + name + ".localhost:8765/manifest.webapp"})
+ ");return true;",
)
wait(name, "return document.readyState==='complete';")
wait(
"system",
"return [...document.querySelectorAll('.appWindow.active[transition-state=opened] iframe')].some(f=>f.src.startsWith('http://"
+ name
+ ".localhost:8765/'));",
)
time.sleep(1)
r["loaded"] = True
r["text"] = ev(name, "return document.body.innerText.slice(0,1800);")
if name == "legacy-2048-for-firefox-os":
wait(name, "return document.querySelectorAll('.tile').length>0;")
r["initialState"] = ev(
name, "return JSON.parse(localStorage.getItem('gameState'));"
)
if args.restart_check:
expected = json.loads((ROOT / "logs/legacy-game-state.json").read_text())
r["stateSurvivesRestart"] = r["initialState"] == expected
else:
tap(name, ".game-container")
focus()
subprocess.run(
[
"xdotool",
"key",
"--delay",
"150",
"Left",
"Up",
"Right",
"Down",
"Left",
"Up",
],
check=True,
)
time.sleep(0.4)
r["keyboardState"] = ev(
name, "return JSON.parse(localStorage.getItem('gameState'));"
)
pos = point(name, ".game-container")
focus()
subprocess.run(
[
"xdotool",
"mousemove",
str(round(pos["x"] + 80)),
str(round(pos["y"])),
"mousedown",
"1",
],
check=True,
)
for i in range(1, 11):
subprocess.run(
[
"xdotool",
"mousemove",
str(round(pos["x"] + 80 - i * 16)),
str(round(pos["y"])),
],
check=True,
)
time.sleep(0.025)
subprocess.run(["xdotool", "mouseup", "1"], check=True)
time.sleep(0.4)
r["swipeState"] = ev(
name, "return JSON.parse(localStorage.getItem('gameState'));"
)
r["keyboardChangesGame"] = r["initialState"] != r["keyboardState"]
r["swipeChangesGame"] = r["keyboardState"] != r["swipeState"]
(ROOT / "logs/legacy-game-state.json").write_text(json.dumps(r["swipeState"]))
r["layout"] = ev(
name,
"const b=document.querySelector('.game-container').getBoundingClientRect();return {width:innerWidth,board:{left:b.left,right:b.right,width:b.width},fits:b.left>=0&&b.right<=innerWidth};",
)
elif name == "legacy-calculator-2":
for key in ["C", "1", "2", "+", "3", "="]:
tap(name, 'input[value="' + key + '"]')
r["display"] = ev(
name, "return document.querySelector('#display div').textContent;"
)
r["twelvePlusThree"] = r["display"].strip() == "15"
else:
wait(name, "return !!document.querySelector('a[href=\"#/podcast/new\"]');")
r["uiReady"] = True
r["text"] = ev(name, "return document.body.innerText;")
r["legacyXhr"] = ev(
name,
"const x=new XMLHttpRequest({mozSystem:true});x.open('GET','/');x.responseType='moz-chunked-arraybuffer';return {mozSystem:typeof x.mozSystem,chunkedType:x.responseType};",
)
tap(name, 'a[href="#/podcast/new"]')
wait(name, "return !!document.querySelector('input');")
r["addForm"] = ev(
name,
"return {text:document.body.innerText,inputs:[...document.querySelectorAll('input')].map(e=>({type:e.type,placeholder:e.placeholder})),buttons:[...document.querySelectorAll('button')].map(e=>({text:e.textContent,type:e.type}))};",
)
if not args.restart_check:
ev(
name,
"window.__archiveRequests=[];const original=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url,...args){const item={method,url:String(url)};window.__archiveRequests.push(item);this.addEventListener('loadend',()=>{item.status=this.status;item.finished=true;});return original.call(this,method,url,...args);};return true;",
)
tap(name, "input")
subprocess.run(
[
"xdotool",
"key",
"ctrl+a",
"type",
"--clearmodifiers",
"--delay",
"15",
"https://atp.fm/episodes?format=rss",
],
check=True,
)
tap(name, "button")
try:
wait(name, "return window.__archiveRequests.some(r=>r.finished);", 20)
except RuntimeError:
r["requestTimedOut"] = True
r["feedRequests"] = ev(name, "return window.__archiveRequests;")
r["afterAdd"] = ev(name, "return document.body.innerText;")
else:
r["afterRestart"] = ev(name, "return document.body.innerText;")
shot(name)
except Exception as e:
r["failure"] = str(e)
try:
shot(name)
except Exception:
pass
r["diagnostics"] = ev(name, "return window.__vulpesDiagnostics || [];")
r["engineConsole"] = client.execute(
"return Services.console.getMessageArray().map(e=>e.message).filter(m=>m.includes("
+ json.dumps(name)
+ ")).slice(-20);"
)
print(name, json.dumps(r, ensure_ascii=False), flush=True)
(
ROOT
/ "logs"
/ ("legacy-apps-restart-results.json" if args.restart_check else "legacy-apps-results.json")
).write_text(json.dumps(results, ensure_ascii=False, indent=2) + "\n")