"""Browser validation for P2-05 chat composer ergonomics (A1-A4). No LLM needed: loads a deterministic fixture, and a real turn fails fast (llama.cpp port refuses the connection) so the error path is exercised quickly. Asserts: - A1: the chat box is a multi-line textarea (max_lines) and is autofocused on load. - A3 (no-session): submitting with no graph preserves the typed draft and shows a "load a graph first" status — the message is NOT committed to the transcript. - A2 + A3 (error): after loading a fixture, submitting a turn keeps the typed text and leaves the box re-enabled once the LLM-down turn errors (proves the disable→re-enable and keep-text-on-error paths, and that the new 6-tuple yields don't crash gradio). - on_build_text empty guard yields cleanly (6-tuple) — clicking Build with no text warns. .venv/Scripts/python.exe scripts/playwright_composer.py Requires `uvicorn loosecanvas.main:app` on APP_PORT (default 8000). """ 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: # noqa: C901 - a linear validation script console: list[str] = [] report: dict[str, object] = {"base": BASE} failures: list[str] = [] warnings: 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) # A1: multi-line textarea + autofocus. tag = box.evaluate("el => el.tagName.toLowerCase()") report["composer_tag"] = tag if tag != "textarea": warnings.append(f"composer is <{tag}>, expected