AI web starter: bands, review split pane, approve gate, server stub (checkpoint)

React + Vite + Tailwind starter with the no-AI shell, band legend, review
split pane with drafts and verify, and a server stub that refuses approval
while flagged fields are unverified. Band self-check and AI page check.
Both page checks share one Chromium via tests/conftest.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 15:06:44 +05:30
1 parent 3eb3fdf00e
commit d4d3f83fb1
24 files changed
+3429 -73

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
"""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(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)