"""Shared by the three page checks (page_check, page_check_ai, page_check_ai_desktop): the modes, the test fill, the fake Qt bridge, the stub loader and the page opener.""" import importlib.util import json import os import sys from pathlib import Path MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)} WIRED = os.environ.get("PAGE_CHECK_WIRED") == "1" TEST_FILL = { "APP_NAME": "Test Bot", "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"}]), "CHECKPOINTS_JSON": json.dumps(["Sign in", "Open the list", "Process items", "Finish up"]), } # Each slot call lands in window.__calls as [slot, ...args]. The AI desktop check exposes # window.__aiRequest, which answers ai_request with an ai_response envelope, as ai_bridge.py does. 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) window.__aiRequest(...args).then(window.__pyMsg); }}); ready({objects: {pyBridge}}); }; """ # The no-AI contract cases every AI page must also pass, unchanged. NO_AI_CASES = [ "test_dark_toggle_flips_theme", "test_donut_and_legend_match_batch_states", "test_kpi_counts_match_batch_states", "test_log_lines_show_their_type", "test_no_bot_page_runs_on_sample_data", "test_plain_browser_first_open_shows_sample_data", "test_report_search_filters_rows_and_shows_bucket_pill", "test_step_list_follows_step_label", ] def no_ai_cases(): """{name: test} for NO_AI_CASES; a test module runs them with globals().update(no_ai_cases()).""" sys.path.insert(0, str(Path(__file__).resolve().parent / "page_check")) import test_contract return {name: getattr(test_contract, name) for name in NO_AI_CASES} def load_stub(site, name="server_stub"): """Import site/server_stub.py. It imports bands.py and pricing.py from its own folder.""" sys.path.insert(0, str(site)) spec = importlib.util.spec_from_file_location(name, Path(site) / "server_stub.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def push_to(pg, event, data): pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data]) def open_page(browser, url, mode, init_script, expose=None): """Generator for a `page` fixture: opens url in one mode, signs in through a login section if the page has one, switches to dark, and fails on any page script error.""" width, height, dark = MODES[mode] ctx = browser.new_context(viewport={"width": width, "height": height}) if expose: ctx.expose_function(*expose) ctx.add_init_script(init_script) pg = ctx.new_page() pg.set_default_timeout(5000) errors = [] pg.on("pageerror", lambda e: errors.append(e)) pg.goto(url) assert not errors, f"page script errors: {errors}" if pg.get_by_role("form", name="Sign in").is_visible(): # login page section: the host lets us in push_to(pg, "login_result", {"ok": True}) if dark: push_to(pg, "apply_dark", {"on": True}) yield pg assert not errors, f"page script errors: {errors}" ctx.close()