Files
Rpa_Skills/tests/page_check_ai/conftest.py
T
VasanthandClaude Opus 5.5 8951ac05a5 AI skill: SKILL.md, AI contract, scrubbed references, eval fixture (d)
- rpa-dashboard-ai/SKILL.md: trigger description and the shared question flow;
  points at the no-AI frontend.md, themes and palette instead of copying them.
- templates/contract.md: runs, review rows, endpoints and the server approve gate.
- Superbill extracts: patient names and the client name removed, every file
  carries an EXAMPLE ONLY banner. tests/test_reference_scan.py checks both skills
  for patient data, internal IPs, credentials and keys.
- tests/fixtures/d-ai-extraction and its eval entry, which runs page_check_ai
  on the built folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:13:55 +05:30

136 lines
4.9 KiB
Python

"""Seam 1 page check for the AI web dashboard (React + Vite), served by its server stub.
Page under test: set PAGE_CHECK_AI_DIR to a generated dashboard folder (the one with
package.json, index.html and server_stub.py). Without it, the AI web starter template
is filled with TEST_FILL and built in a cache folder. Either way the folder is built
with npm, and its server_stub.py runs in this process, so tests can set run and
review state directly through the `stub` fixture.
Contract envelopes go in through window.__pyMsg, as in the no-AI check. Each action
the page POSTs to {API_BASE}/<slot> is recorded in window.__calls as [slot, ...args].
Every page test runs in light, dark and narrow mode.
"""
import hashlib
import importlib.util
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import threading
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
TEMPLATE = ROOT / "rpa-dashboard-ai" / "templates" / "web"
TEST_FILL = {
"APP_NAME": "Test Extractor",
"APP_SUBTITLE": "Page check",
"LOGO_SRC": "",
"API_BASE": "/api",
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
}
RECORD_ACTIONS = """
window.__calls = [];
const realFetch = window.fetch;
window.fetch = (url, opts = {}) => {
const m = String(url).match(/\\/api\\/([a-z-]+)$/);
if (m && opts.method === 'POST') window.__calls.push([m[1].replace(/-/g, '_'), ...JSON.parse(opts.body || '[]')]);
return realFetch(url, opts);
};
"""
MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)}
SKIP_COPY = shutil.ignore_patterns("node_modules", "dist", "__pycache__")
@pytest.fixture
def sample_data():
"""Shared no-AI cases that need the page's own sample data skip on a bot-wired page."""
if os.environ.get("PAGE_CHECK_WIRED") == "1":
pytest.skip("page is wired to a bot, so it has no sample data")
@pytest.fixture(scope="session")
def site():
"""The dashboard folder under test: server_stub.py, index.html and the sources."""
if os.environ.get("PAGE_CHECK_AI_DIR"):
return Path(os.environ["PAGE_CHECK_AI_DIR"]).resolve()
pkg = (TEMPLATE / "package.json").read_bytes() + (TEMPLATE / "package-lock.json").read_bytes()
out = Path(tempfile.gettempdir()) / f"rpa-dashboard-ai-web-{hashlib.sha1(pkg).hexdigest()[:10]}"
shutil.rmtree(out / "src", ignore_errors=True) # node_modules is kept between runs
shutil.copytree(TEMPLATE, out, dirs_exist_ok=True, ignore=SKIP_COPY)
index = out / "index.html"
index.write_text(re.sub(r"\{\{(\w+)\}\}", lambda m: TEST_FILL[m[1]], index.read_text(encoding="utf-8")),
encoding="utf-8")
return out
@pytest.fixture(scope="session")
def built(site):
npm = shutil.which("npm") or pytest.skip("npm not on PATH")
if not (site / "node_modules").exists():
subprocess.run([npm, "ci" if (site / "package-lock.json").exists() else "install"], cwd=site, check=True)
r = subprocess.run([npm, "run", "build"], cwd=site, capture_output=True, text=True)
assert r.returncode == 0, r.stdout + r.stderr
return site
@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("server_stub", site / "server_stub.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@pytest.fixture(scope="session")
def server(stub_module):
srv = stub_module.make_server("127.0.0.1", 0)
threading.Thread(target=srv.serve_forever, daemon=True).start()
yield f"http://127.0.0.1:{srv.server_address[1]}"
srv.shutdown()
@pytest.fixture
def stub(stub_module, server):
"""The running stub, reset to its sample data. Tests change stub.RUNS / stub.ROWS directly."""
stub_module.reset()
return stub_module
@pytest.fixture(scope="session")
def page_url(built, server):
return server + "/"
@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.add_init_script(RECORD_ACTIONS)
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)