checking system…
Docs / back / tests/dashboard/test_arena_page_js.py · line 1
Python · 142 lines
  1"""Guard against JS syntax errors in every dashboard HTML page.
  2
  3Inline ``<script>`` blocks in the templates have, in practice, been the
  4single largest source of "the whole tab stopped working" bugs. One stray
  5comma or mis-placed ``+`` halts JS parsing — every onclick handler then
  6silently fails with ``ReferenceError``, and the user sees a page that
  7looks fine but does nothing when clicked.
  8
  9This test renders every HTML route through the TestClient, extracts each
 10``<script>`` block from the response, and runs ``node --check`` on it.
 11Parametrized one route per test so CI output points straight at the
 12broken page.
 13
 14If node isn't on PATH the tests skip (rather than failing — we don't
 15want CI to go red because a runner lacks node).
 16"""
 17
 18from __future__ import annotations
 19
 20import re
 21import shutil
 22import subprocess
 23import tempfile
 24from pathlib import Path
 25from unittest.mock import MagicMock
 26
 27import pytest
 28from fastapi.testclient import TestClient
 29
 30from maf.config import ArenaConfig
 31from maf.dashboard import api as dashboard_api
 32
 33
 34_SCRIPT_RE = re.compile(r"<script[^>]*>(.*?)</script>", re.DOTALL)
 35
 36
 37def _node_available() -> bool:
 38    return shutil.which("node") is not None
 39
 40
 41# Every HTML route under the dashboard. ``arena`` is templated so we hit
 42# it with a known arena name. Add a row here when a new HTML page lands.
 43_HTML_ROUTES: list[str] = [
 44    "/",
 45    "/arenas/demo",
 46    "/channels",
 47    "/data",
 48    "/live",
 49    "/llm",
 50    "/mastermind",
 51    "/oracle",
 52    "/sources",
 53    "/modules",
 54    "/wizard",
 55    "/docs",
 56]
 57
 58
 59@pytest.fixture
 60def fake_arena(tmp_path: Path, monkeypatch):
 61    """Wire a minimal arena onto the dashboard so every page renders.
 62
 63    The pages reach into ``maf_app.modules`` / ``maf_app.list_arenas()`` /
 64    ``maf_app.get_arena()`` — supply enough mock to make those calls
 65    return sensible data without spinning up the full runtime.
 66    """
 67    arena_dir = tmp_path / "arenas"
 68    arena_dir.mkdir()
 69    valid = {
 70        "name": "demo",
 71        "description": "test",
 72        "schedule": None,
 73        "max_iterations": 1,
 74        "target_key": "ticker",
 75        "sources": [],
 76        "phases": [{
 77            "name": "analysis", "pattern": "sequential", "agents": [],
 78            "max_rounds": 1, "transition": "END",
 79        }],
 80        "triggers": [],
 81    }
 82    arena = MagicMock()
 83    arena.config = ArenaConfig(**valid)
 84
 85    maf_app = MagicMock()
 86    maf_app.config.arena_dir = str(arena_dir)
 87    maf_app.config.modules = []
 88    maf_app.modules = []  # so /modules iterates empty
 89    maf_app.get_arena.return_value = arena
 90    maf_app.list_arenas.return_value = ["demo"]
 91    monkeypatch.setattr(dashboard_api, "_maf_app", maf_app)
 92    return maf_app
 93
 94
 95def _node_check(script: str) -> str | None:
 96    """Run ``node --check`` on a snippet. Return ``None`` on success,
 97    the first 5 lines of stderr otherwise."""
 98    with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False) as fh:
 99        fh.write(script)
100        tmp = fh.name
101    try:
102        proc = subprocess.run(
103            ["node", "--check", tmp],
104            capture_output=True, text=True, timeout=10,
105        )
106        if proc.returncode == 0:
107            return None
108        return "\n".join(proc.stderr.splitlines()[:5])
109    finally:
110        Path(tmp).unlink(missing_ok=True)
111
112
113@pytest.mark.skipif(not _node_available(), reason="node not installed — JS syntax check skipped")
114@pytest.mark.parametrize("path", _HTML_ROUTES)
115def test_html_page_inline_js_parses(fake_arena, path: str) -> None:
116    """Every inline <script> block on this page must be valid JavaScript.
117
118    Catches the kind of bug where a hand-rolled ternary inside template-
119    string concatenation breaks the parser — and with it every onclick
120    handler on the page.
121    """
122    with TestClient(dashboard_api.app) as client:
123        r = client.get(path, follow_redirects=True)
124    assert r.status_code == 200, f"{path} returned {r.status_code}"
125
126    scripts = _SCRIPT_RE.findall(r.text)
127    if not scripts:
128        # Some pages (e.g. /docs) may have no inline script — that's fine.
129        pytest.skip(f"{path} has no inline <script> blocks")
130
131    failures: list[str] = []
132    for i, script in enumerate(scripts):
133        if not script.strip():
134            continue
135        err = _node_check(script)
136        if err is not None:
137            failures.append(f"  <script> #{i + 1}:\n{err}")
138
139    assert not failures, (
140        f"JS parse errors on {path}:\n" + "\n\n".join(failures)
141    )