smolcode / engine /browser_runner.py
seanpoyner's picture
Upload folder using huggingface_hub
daea45b verified
Raw
History Blame Contribute Delete
6.15 kB
"""Subprocess runner: check a model-built web app in a REAL headless browser.
Invoked as `python engine/browser_runner.py <app.html>` 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 = ("<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>")
def _escape_srcdoc(doc: str) -> str:
return doc.replace("&", "&amp;").replace('"', "&quot;")
def _inject_capture(app_html: str) -> str:
"""Put the error collector before the app's own scripts."""
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 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 = ('<!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="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]))