"""Browser validation for C1 (last-turn spotlight build health) + C2 (up-arrow recall). No LLM needed. Asserts: - C1 build health: the freshly-built component loads, the fixture renders into window.__cy, and the patch-apply path produces no console errors (the spotlight wiring lives in that path; its node-derivation logic is unit-tested in vitest). - C2 up-arrow recall: typing + Enter records sent messages; ArrowUp/ArrowDown cycle them back into the (cleared) composer; the recalled text is what would be sent. .venv/Scripts/python.exe scripts/playwright_c1c2.py Requires `uvicorn loosecanvas.main:app` on APP_PORT (default 8000), built from the current frontend. """ from __future__ import annotations import json import os import sys from pathlib import Path from playwright.sync_api import sync_playwright BASE = f"http://127.0.0.1:{os.environ.get('APP_PORT', '8000')}/" OUT = Path(__file__).resolve().parent.parent / ".playwright-out" OUT.mkdir(exist_ok=True) PLACEHOLDER = "Ask about the graph" def main() -> int: console: list[str] = [] report: dict[str, object] = {"base": BASE} failures: list[str] = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1400, "height": 900}) page.on("console", lambda m: console.append(f"{m.type}: {m.text}")) page.on("pageerror", lambda e: console.append(f"pageerror: {e}")) page.add_init_script("window.__PLAYWRIGHT_TEST = true;") page.goto(BASE, wait_until="domcontentloaded") page.wait_for_selector("#lc-canvas", timeout=30_000) box = page.get_by_placeholder(PLACEHOLDER) box.wait_for(timeout=15_000) # ── C1 build health: fixture renders into the freshly-built component ── page.get_by_role("button", name="Load graph").click() page.wait_for_function( "() => window.__cy && window.__cy.nodes().length > 0", timeout=20_000 ) node_count = page.evaluate("() => window.__cy.nodes().length") report["fixture_node_count"] = node_count # The spotlight's node-derivation logic is unit-tested (renderer.test.ts) and the # .flash rule it reuses is asserted in cytoscapeStyle there; here we confirm the # freshly-built component renders and the patch path stays console-clean. # ── C2 up-arrow recall ── box.click() page.keyboard.press("Control+A") page.keyboard.type("alpha question") page.keyboard.press("Enter") # JS records "alpha question" page.wait_for_timeout(300) box.click() page.keyboard.press("Control+A") page.keyboard.type("beta question") page.keyboard.press("Enter") # JS records "beta question" page.wait_for_timeout(300) # Clear the composer, then recall. box.click() page.keyboard.press("Control+A") page.keyboard.press("Delete") report["after_clear"] = box.input_value() page.keyboard.press("ArrowUp") # → most recent: "beta question" page.wait_for_timeout(120) recall1 = box.input_value() report["recall_1"] = recall1 page.keyboard.press("ArrowUp") # → "alpha question" page.wait_for_timeout(120) recall2 = box.input_value() report["recall_2"] = recall2 page.keyboard.press("ArrowDown") # → back to "beta question" page.wait_for_timeout(120) down1 = box.input_value() report["arrowdown_1"] = down1 page.screenshot(path=str(OUT / "c2_recall.png")) if recall1 != "beta question": failures.append( f"C2: first ArrowUp expected 'beta question', got {recall1!r}" ) if recall2 != "alpha question": failures.append( f"C2: second ArrowUp expected 'alpha question', got {recall2!r}" ) if down1 != "beta question": failures.append(f"C2: ArrowDown expected 'beta question', got {down1!r}") browser.close() errors = [c for c in console if c.startswith(("error:", "pageerror:"))] report["console_errors"] = errors if errors: failures.append(f"{len(errors)} console/page error(s): {errors[:3]}") report["failures"] = failures report["ok"] = not failures (OUT / "c1c2_report.json").write_text( json.dumps(report, indent=2), encoding="utf-8" ) print(json.dumps(report, indent=2)) return 0 if not failures else 1 if __name__ == "__main__": sys.exit(main())