Spaces:
Paused
Paused
File size: 6,152 Bytes
daea45b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """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("&", "&").replace('"', """)
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]))
|