"""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}/ is recorded in window.__calls as [slot, ...args]. Every page test runs in light, dark and narrow mode. Shared helpers: tests/page_check_common.py. """ import hashlib import os import re import shutil import subprocess import tempfile import threading from pathlib import Path import pytest from page_check_common import MODES, TEST_FILL, load_stub, open_page ROOT = Path(__file__).resolve().parents[2] TEMPLATE = ROOT / "skills" / "rpa-dashboard-ai" / "templates" / "web" TEST_FILL = {**TEST_FILL, "APP_NAME": "Test Extractor", "API_BASE": "/api"} 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); }; """ 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): return load_stub(site) @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): yield from open_page(browser, page_url, request.param, RECORD_ACTIONS)