Spaces:
Paused
Paused
| """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("&", "&").replace('"', """) | |
| 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])) | |