"""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 ` — 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 = ("") _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("&", "&").replace('"', """) def _inject_capture(app_html: str) -> str: m = re.search(r"]*>", app_html, re.I) if m: return app_html[:m.end()] + _CAPTURE + app_html[m.end():] m = re.search(r"]*>", 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 = ('' f'') 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]))