loosecanvas / scripts /playwright_c1c2.py
Joshua Sundance Bailey
loosecanvas: local AI thought-mapping canvas with a trust-tagged knowledge graph
6d1438c
Raw
History Blame Contribute Delete
4.6 kB
"""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())