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("&", "&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]))