"""Subprocess runner: check a model-built web app in a REAL headless browser. Invoked as `python engine/browser_runner.py ` by engine/browsercheck.py — never imported (keeps it free of the engine package / liteforge, and isolates a browser crash from the Gradio process). It loads the app wrapped in the EXACT same `srcdoc` + `sandbox` as the live preview (engine/preview.py), so the verdict matches what the user sees, then clicks every button and exercises the keyboard, and reports any uncaught JavaScript errors. Browser: headless Firefox via Selenium + geckodriver. (Playwright's browser CDN is firewalled in this environment; conda-forge Firefox is the reachable, rootless real browser. The choice is invisible to callers — same JSON contract.) We capture errors by injecting a tiny `window.onerror`/`unhandledrejection` collector at the top of the framed document (so it catches errors during initial script execution — the "script ran before its element / undefined function" class), then read it back. That is the HARD failure signal. Output: one JSON line {ok, errors, buttons, clicked}. Exit 3 only when the browser itself can't run, so the caller can fall back to the jsdom checker. """ import json import os import re import sys import tempfile PREVIEW_SANDBOX = "allow-scripts allow-same-origin allow-modals allow-popups allow-forms" # Installed by the rootless conda-forge setup (see DEVELOPING.md). Overridable. _BROWSER_PREFIX = os.environ.get( "SMOLBUILDER_BROWSER_PREFIX", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".browser")) _FIREFOX_BIN = os.path.join(_BROWSER_PREFIX, "bin", "FirefoxApp", "firefox") _GECKODRIVER = os.path.join(_BROWSER_PREFIX, "bin", "geckodriver") # Injected first inside the frame so it catches errors thrown during load. _CAPTURE = ("") def _escape_srcdoc(doc: str) -> str: return doc.replace("&", "&").replace('"', """) def _inject_capture(app_html: str) -> str: """Put the error collector before the app's own scripts.""" 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 selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service from selenium.webdriver.common.by import By except Exception as e: _emit({"ok": None, "infra": f"selenium import failed: {e}"}) return 3 if not (os.path.exists(_FIREFOX_BIN) and os.path.exists(_GECKODRIVER)): _emit({"ok": None, "infra": "firefox/geckodriver not installed"}) return 3 with open(path, encoding="utf-8") as f: app_html = f.read() host = ('' f'') host_path = os.path.join(tempfile.mkdtemp(prefix="brhost-"), "host.html") with open(host_path, "w", encoding="utf-8") as f: f.write(host) opts = Options() opts.add_argument("-headless") opts.binary_location = _FIREFOX_BIN opts.set_preference("security.sandbox.content.level", 0) # no userns in container svc = Service(executable_path=_GECKODRIVER, log_output=os.path.join(tempfile.gettempdir(), "gecko.log")) try: driver = webdriver.Firefox(options=opts, service=svc) except Exception as e: _emit({"ok": None, "infra": f"firefox launch failed: {str(e)[:200]}"}) return 3 errors: list[str] = [] buttons = clicked = 0 try: driver.set_page_load_timeout(20) driver.get("file://" + host_path) driver.switch_to.frame(driver.find_element(By.ID, "app")) import time time.sleep(0.3) # let scripts settle els = driver.find_elements( By.CSS_SELECTOR, "button, [onclick], input[type=button], input[type=submit]") buttons = len(els) for el in els[:25]: try: driver.execute_script("arguments[0].disabled=false;", el) el.click() clicked += 1 except Exception: pass # handler errors show up in __errs # Exercise keyboard handlers (canvas games etc.). try: driver.execute_script( "['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));});") except Exception: pass time.sleep(0.3) # surface late/timer errors try: errors = driver.execute_script("return window.__errs || [];") or [] except Exception: errors = [] finally: try: driver.quit() except Exception: pass 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]))