MODES, TEST_FILL, FAKE_BRIDGE, the stub loader, the page opener and the no-AI case list now live in one place; sample_data and push move to tests/conftest.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""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()
|