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>
76 lines
3.3 KiB
Python
76 lines
3.3 KiB
Python
"""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 </body>.
|
|
|
|
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 shared
|
|
TEST_FILL (tests/page_check_common.py) with no optional sections (sections.py) and the AI
|
|
sections are pasted in, the way the skill does it.
|
|
|
|
The page sees the shared fake Qt bridge: 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
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from page_check_common import FAKE_BRIDGE, MODES, TEST_FILL, WIRED, load_stub, open_page
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
NO_AI = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
|
AI = ROOT / "rpa-dashboard-ai" / "templates"
|
|
|
|
|
|
@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()
|
|
spec = importlib.util.spec_from_file_location("sections", NO_AI.with_name("sections.py"))
|
|
sections = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(sections)
|
|
html = sections.keep(NO_AI.read_text(encoding="utf-8"), []) # no optional sections picked
|
|
html = re.sub(r"\{\{(\w+)\}\}", lambda m: TEST_FILL[m[1]], html)
|
|
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("</body>", sections + "\n</body>"), 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):
|
|
return load_stub(site, "desktop_server_stub")
|
|
|
|
|
|
@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):
|
|
expose = ("__aiRequest", lambda i, method, path, body: ai_request(stub, i, method, path, body))
|
|
yield from open_page(browser, page_url, request.param, FAKE_BRIDGE, expose)
|