Files
Seb Hirsch a0ec2d5b8b Folder batch loader, Save Image source modes, headless test suite
Load Images From Folder walks a folder one image per run, queueing the next itself once the current run has finished, or emits every image as a list. Save Image (Seb) can save next to the source or replace it in place, atomically, keeping the format. tests/ drives a real headless Chromium against a scratch ComfyUI; see tests/README.md.
2026-09-13 17:10:58 +02:00

121 lines
4.4 KiB
Python

"""Autonomous end-to-end check of Read360Seb + the vendored Pano Editor.
Runs headless Chromium (which actually composites, unlike the embedded pane, so
requestAnimationFrame fires and the editor paints), builds a graph, executes it,
opens the editor and measures whether the panorama is really on screen.
"""
import json, sys, time, os
from playwright.sync_api import sync_playwright
PORT = int(os.environ.get("PANO_PORT", "8189"))
URL = f"http://127.0.0.1:{PORT}"
OUT = os.path.dirname(os.path.abspath(__file__))
IMAGE = os.environ.get("PANO_IMAGE", "seb_360_testpano.png")
BUILD = """
async ({image}) => {
const app = window.app;
app.graph.clear();
const n = LiteGraph.createNode("Read360Seb"); app.graph.add(n); n.pos=[40,40];
const p = LiteGraph.createNode("PreviewImage"); app.graph.add(p); p.pos=[620,40];
n.connect(0, p, 0);
const g = k => n.widgets.find(w => w.name === k);
g("image").value = image;
g("canvas_width").value = 2048;
g("view_width").value = 768; g("view_height").value = 768;
g("fov").value = 60; g("yaw").value = 0; g("pitch").value = 0;
app.graph.setDirtyCanvas(true, true);
await app.queuePrompt(0, 1);
return {nodeId: n.id};
}
"""
SAMPLE = """
() => {
const out = [];
for (const c of document.querySelectorAll('canvas')) {
if (c.width < 200 || c.height < 150) continue;
let mean = null, mode = '';
try {
const g2 = c.getContext('2d');
if (g2) {
mode = '2d';
const w = Math.min(120, c.width), h = Math.min(120, c.height);
const d = g2.getImageData((c.width-w)>>1, (c.height-h)>>1, w, h).data;
let s = 0, n = 0;
for (let i = 0; i < d.length; i += 4) { s += d[i]+d[i+1]+d[i+2]; n += 3; }
mean = +(s/n).toFixed(1);
}
} catch (e) { mode = 'err'; }
if (mean === null) {
const gl = c.getContext('webgl2') || c.getContext('webgl');
if (gl) {
mode = 'webgl';
const px = new Uint8Array(4*400);
gl.readPixels((c.width>>1)-10, (c.height>>1)-10, 20, 20, gl.RGBA, gl.UNSIGNED_BYTE, px);
let s=0,n=0; for (let i=0;i<px.length;i+=4){s+=px[i]+px[i+1]+px[i+2];n+=3;}
mean = +(s/n).toFixed(1);
}
}
out.push({w:c.width, h:c.height, mode, mean});
}
return out;
}
"""
def main():
report = {}
with sync_playwright() as pw:
b = pw.chromium.launch(headless=True, args=[
"--use-gl=swiftshader", "--enable-unsafe-swiftshader",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding", "--disable-background-timer-throttling",
])
pg = b.new_page(viewport={"width": 1600, "height": 1000})
logs = []
pg.on("console", lambda m: logs.append(f"{m.type}: {m.text[:160]}"))
pg.on("pageerror", lambda e: logs.append(f"pageerror: {str(e)[:160]}"))
pg.goto(URL, wait_until="domcontentloaded", timeout=60000)
pg.wait_for_function("() => !!(window.app && window.app.graph)", timeout=90000)
time.sleep(3)
report["build"] = pg.evaluate(BUILD, {"image": IMAGE})
# wait for execution to land
for _ in range(60):
got = pg.evaluate("(id)=>{const o=window.app.nodeOutputs||{};return o[id]||o[String(id)]||null;}",
report["build"]["nodeId"])
if got:
report["executed"] = {k: got[k] for k in got if k != "images"}
break
time.sleep(1)
else:
report["executed"] = None
report["before_editor"] = pg.evaluate(SAMPLE)
pg.screenshot(path=os.path.join(OUT, "pano_node.png"))
pg.evaluate("""() => {
const n = window.app.graph._nodes.find(x=>x.type==='Read360Seb');
const b = n.widgets.find(w=>/Open Pano Editor/.test(w.name||''));
if (b && b.callback) b.callback();
}""")
time.sleep(7)
report["editor_open"] = pg.evaluate(
"() => document.querySelectorAll('[class*=\"pano\"]').length > 50")
report["after_editor"] = pg.evaluate(SAMPLE)
pg.screenshot(path=os.path.join(OUT, "pano_editor.png"))
report["console_tail"] = logs[-8:]
b.close()
lit = [c for c in report.get("after_editor", []) if (c.get("mean") or 0) > 8]
report["VERDICT_editor_shows_content"] = bool(lit)
report["lit_canvases"] = lit
print(json.dumps(report, indent=1)[:2600])
sys.exit(0 if report["VERDICT_editor_shows_content"] else 1)
if __name__ == "__main__":
main()