File size: 5,682 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
"""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
<app.html>` — 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 = ("<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>")

_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("&", "&amp;").replace('"', "&quot;")


def _inject_capture(app_html: str) -> str:
    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 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 = ('<!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="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]))