- templates/bot_runner.py (new, no Qt): runs the bot as a child process (unbuffered, UTF-8), tails its output, keeps raw lines in bot_raw.log, and turns lines into step/log/status events. Step markers and friendly rules use specific substrings, first match wins; unmatched error lines are always shown, other unmatched lines hidden. Dry run: python dashboard/bot_runner.py prints the events the page would get. - bridge.py: delegates to bot_runner, one run at a time, restores the saved dark mode on every page load (fixes sample-data mode in PyQt), adds load_latest_report / download_report slots the page calls. - SKILL.md: skip the stack question for a PyQt project; desktop wiring steps from the bot's real output; verify steps for the host. - tests: assert-based self-check for the line logic and run loop; Seam 1 run replay (status dot, Run button, steps, log) and wired-page mode; fixture b-pyqt-bot and its Seam 2 eval (all 14 checks pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""Seam 1 page check: load a dashboard page with no backend, push contract envelopes
|
|
through window.__pyMsg, and assert on what is visible.
|
|
|
|
Page under test: set PAGE_CHECK_HTML to a generated dashboard.html. Without it, the
|
|
no-AI template is filled with TEST_FILL. Every test runs in light, dark and narrow mode.
|
|
The page sees a fake Qt bridge: each slot call lands in window.__calls as [slot, ...args].
|
|
PAGE_CHECK_WIRED=1: the page is wired to a bot (SAMPLE_DATA off), so sample-data cases skip.
|
|
PAGE_CHECK_REPLAY: a JSON-lines file of envelopes a real bot run produced (bot_runner.py's dry run).
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
TEMPLATE = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
|
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"}]),
|
|
}
|
|
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]); }});
|
|
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():
|
|
"""Use on cases that need the built-in sample data; they skip on a bot-wired page."""
|
|
if WIRED:
|
|
pytest.skip("page is wired to a bot, so it has no sample data")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def page_url(tmp_path_factory):
|
|
if os.environ.get("PAGE_CHECK_HTML"):
|
|
return Path(os.environ["PAGE_CHECK_HTML"]).resolve().as_uri()
|
|
html = re.sub(r"\{\{(\w+)\}\}", lambda m: TEST_FILL[m[1]], TEMPLATE.read_text(encoding="utf-8"))
|
|
if WIRED:
|
|
html = html.replace("const SAMPLE_DATA = true", "const SAMPLE_DATA = false")
|
|
out = tmp_path_factory.mktemp("page") / "dashboard.html"
|
|
out.write_text(html, encoding="utf-8")
|
|
return out.as_uri()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def browser():
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
yield b
|
|
b.close()
|
|
|
|
|
|
@pytest.fixture(params=list(MODES))
|
|
def page(request, browser, page_url):
|
|
width, height, dark = MODES[request.param]
|
|
ctx = browser.new_context(viewport={"width": width, "height": height})
|
|
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
|
|
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):
|
|
"""push(event, data): send one contract envelope to the page, as the bot would."""
|
|
return lambda event, data: push_to(page, event, data)
|