smolcode / engine /playwright_runner.py
seanpoyner's picture
Upload folder using huggingface_hub
daea45b verified
Raw
History Blame Contribute Delete
5.68 kB
"""Subprocess runner: check a model-built web app in headless Chromium.
A Playwright/Chromium sibling of engine/browser_runner.py (Firefox/Selenium),
with the IDENTICAL JSON contract so engine/browsercheck.py can try whichever
real browser is installed. Invoked as `python engine/playwright_runner.py
<app.html>` — never imported (keeps Playwright out of the Gradio process and
isolates a browser crash).
It loads the app in the EXACT same `srcdoc` + `sandbox` wrapper as the live
preview (engine/preview.py), injects an error collector before the app's own
scripts, clicks every button, exercises the keyboard, and reports uncaught JS
errors — the hard failure signal that lets the router escalate a broken build.
Output: one JSON line {ok, errors, buttons, clicked}. Exit 3 only when Chromium
itself can't run (Playwright missing or the browser binary not downloaded), so
the caller falls back to Firefox, then jsdom.
"""
import json
import os
import re
import sys
import tempfile
PREVIEW_SANDBOX = "allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
# Same collector browser_runner.py injects: catches errors thrown during load
# (the "script ran before its element / undefined function" class).
_CAPTURE = ("<script>(function(){window.__errs=[];"
"window.addEventListener('error',function(e){try{__errs.push('uncaught: '+"
"((e.error&&e.error.message)||e.message||String(e)))}catch(_){}} ,true);"
"window.addEventListener('unhandledrejection',function(e){try{__errs.push("
"'rejection: '+((e.reason&&e.reason.message)||e.reason))}catch(_){}});})();</script>")
_CLICK_SELECTOR = "button, [onclick], input[type=button], input[type=submit]"
_KEYBOARD_JS = (
"['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].forEach(function(k){"
"var c={key:k,keyCode:k===' '?32:({ArrowUp:38,ArrowDown:40,ArrowLeft:37,ArrowRight:39}[k]),bubbles:true};"
"document.dispatchEvent(new KeyboardEvent('keydown',c));"
"window.dispatchEvent(new KeyboardEvent('keydown',c));});")
def _escape_srcdoc(doc: str) -> str:
return doc.replace("&", "&amp;").replace('"', "&quot;")
def _inject_capture(app_html: str) -> str:
m = re.search(r"<head[^>]*>", app_html, re.I)
if m:
return app_html[:m.end()] + _CAPTURE + app_html[m.end():]
m = re.search(r"<html[^>]*>", app_html, re.I)
if m:
return app_html[:m.end()] + _CAPTURE + app_html[m.end():]
return _CAPTURE + app_html
def _emit(obj: dict) -> None:
sys.stdout.write(json.dumps(obj) + "\n")
def main(path: str) -> int:
try:
from playwright.sync_api import sync_playwright
except Exception as e: # noqa: BLE001
_emit({"ok": None, "infra": f"playwright import failed: {e}"})
return 3
with open(path, encoding="utf-8") as f:
app_html = f.read()
host = ('<!doctype html><meta charset="utf-8"><body style="margin:0">'
f'<iframe id="app" style="width:100%;height:600px;border:0" '
f'sandbox="{PREVIEW_SANDBOX}" '
f'srcdoc="{_escape_srcdoc(_inject_capture(app_html))}"></iframe>')
host_path = os.path.join(tempfile.mkdtemp(prefix="pwhost-"), "host.html")
with open(host_path, "w", encoding="utf-8") as f:
f.write(host)
errors: list[str] = []
buttons = clicked = 0
try:
with sync_playwright() as p:
try:
browser = p.chromium.launch(
headless=True,
args=["--allow-file-access-from-files", "--no-sandbox"])
except Exception as e: # noqa: BLE001
_emit({"ok": None, "infra": f"chromium launch failed: {str(e)[:200]}"})
return 3
try:
page = browser.new_page()
page.set_default_timeout(4000)
page.goto("file://" + host_path, timeout=20000)
handle = page.wait_for_selector("#app", timeout=5000)
frame = handle.content_frame()
if frame is None:
_emit({"ok": None, "infra": "could not enter app iframe"})
return 3
page.wait_for_timeout(300) # let scripts settle
els = frame.query_selector_all(_CLICK_SELECTOR)
buttons = len(els)
for el in els[:25]:
try:
el.evaluate("e => { e.disabled = false; }")
el.click(force=True, timeout=1000)
clicked += 1
except Exception:
pass # handler errors land in __errs
try:
frame.evaluate(_KEYBOARD_JS)
except Exception:
pass
page.wait_for_timeout(300) # surface late/timer errors
try:
errors = frame.evaluate("() => window.__errs || []") or []
except Exception:
errors = []
finally:
try:
browser.close()
except Exception:
pass
except Exception as e: # noqa: BLE001
_emit({"ok": None, "infra": f"playwright run failed: {str(e)[:200]}"})
return 3
errors = [str(e)[:400] for e in errors][:20]
_emit({"ok": len(errors) == 0, "errors": errors, "buttons": buttons, "clicked": clicked})
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))