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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

253
tools/test-legacy-apps.py Normal file
View file

@ -0,0 +1,253 @@
#!/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")