"""Seam 1 page check for the AI desktop page: the no-AI dashboard.html with the plain-HTML AI sections (rpa-dashboard-ai/templates/desktop/ai_sections.html) pasted in before . Page under test: set PAGE_CHECK_AI_DESKTOP_DIR to a generated dashboard folder (the one with dashboard.html and server_stub.py). Without it, the no-AI template is filled with the no-AI check's TEST_FILL and the AI sections are pasted in, the way the skill does it. The page sees a fake Qt bridge, as in the no-AI check: each slot call lands in window.__calls. The AI slot, ai_request(id, method, path, body), goes to the review backend's dispatch() in this process, and the reply comes back as an `ai_response` envelope, exactly what ai_bridge.py's slot does. So the approve gate under test is the Python one. Tests set run and review state on the backend directly (the `stub` fixture). Light, dark and narrow for every case. """ import importlib.util import json import os import re import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] NO_AI = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html" AI = ROOT / "rpa-dashboard-ai" / "templates" TEST_FILL = { "APP_NAME": "Test Extractor", "APP_SUBTITLE": "Page check", "LOGO_SRC": "", "API_BASE": "", "STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]), "REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]), } FAKE_BRIDGE = """ window.__calls = []; window.qt = {webChannelTransport: {}}; window.QWebChannel = function (_transport, ready) { const pyBridge = new Proxy({}, {get: (_o, slot) => (...args) => { window.__calls.push([slot, ...args]); if (slot === 'ai_request') window.__aiRequest(...args).then(window.__pyMsg); }}); ready({objects: {pyBridge}}); }; """ MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)} WIRED = os.environ.get("PAGE_CHECK_WIRED") == "1" @pytest.fixture def sample_data(): if WIRED: pytest.skip("page is wired to a bot, so it has no sample data") @pytest.fixture(scope="session") def site(tmp_path_factory): """The dashboard folder under test: dashboard.html, server_stub.py and bands.py.""" if os.environ.get("PAGE_CHECK_AI_DESKTOP_DIR"): return Path(os.environ["PAGE_CHECK_AI_DESKTOP_DIR"]).resolve() html = re.sub(r"\{\{(\w+)\}\}", lambda m: TEST_FILL[m[1]], NO_AI.read_text(encoding="utf-8")) if WIRED: html = html.replace("const SAMPLE_DATA = true", "const SAMPLE_DATA = false") sections = (AI / "desktop" / "ai_sections.html").read_text(encoding="utf-8") out = tmp_path_factory.mktemp("desktop") (out / "dashboard.html").write_text(html.replace("", sections + "\n"), encoding="utf-8") for f in ["server_stub.py", "bands.py", "pricing.py"]: (out / f).write_bytes((AI / "web" / f).read_bytes()) return out @pytest.fixture(scope="session") def page_url(site): return (site / "dashboard.html").as_uri() @pytest.fixture(scope="session") def stub_module(site): sys.path.insert(0, str(site)) # the stub imports bands.py from its own folder spec = importlib.util.spec_from_file_location("desktop_server_stub", site / "server_stub.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod @pytest.fixture def stub(stub_module): """The review backend the bridge talks to, reset to its sample data.""" stub_module.reset() return stub_module def ai_request(stub, req_id, method, path, body): """What ai_bridge.py's ai_request slot does: dispatch, then answer with one envelope.""" status, payload = stub.dispatch(method, path, json.loads(body or "{}")) return json.dumps({"event": "ai_response", "data": {"id": req_id, "status": status, "body": payload}}) @pytest.fixture(params=list(MODES)) def page(request, browser, page_url, stub): width, height, dark = MODES[request.param] ctx = browser.new_context(viewport={"width": width, "height": height}) ctx.expose_function("__aiRequest", lambda i, method, path, body: ai_request(stub, i, method, path, body)) ctx.add_init_script(FAKE_BRIDGE) pg = ctx.new_page() pg.set_default_timeout(5000) errors = [] pg.on("pageerror", lambda e: errors.append(e)) pg.goto(page_url) assert not errors, f"page script errors: {errors}" if dark: push_to(pg, "apply_dark", {"on": True}) yield pg assert not errors, f"page script errors: {errors}" ctx.close() def push_to(pg, event, data): pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data]) @pytest.fixture def push(page): return lambda event, data: push_to(page, event, data)