Joshua Sundance Bailey
loosecanvas: local AI thought-mapping canvas with a trust-tagged knowledge graph
6d1438c | """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()) | |