# /// script # requires-python = ">=3.11" # dependencies = ["playwright"] # /// """Browser end-to-end test: loads the UI, sends a message with Enter, waits for the streamed reply to finish rendering, and saves screenshots. uv run scripts/e2e.py [--url http://127.0.0.1:3000] [--channel msedge] """ import argparse, sys, time from playwright.sync_api import sync_playwright, expect p = argparse.ArgumentParser() p.add_argument("--url", default="http://127.0.0.1:3000") p.add_argument("--channel", default="msedge", help="installed browser: chrome, msedge, ...") p.add_argument("--out", default="docs") p.add_argument("--dark", action="store_true") p.add_argument("--prompt", default="Give me 3 short tips for learning Go, as a numbered list. Include one tiny code snippet.") args = p.parse_args() with sync_playwright() as pw: browser = pw.chromium.launch(channel=args.channel, headless=True) page = browser.new_page(viewport={"width": 900, "height": 760}, color_scheme="dark" if args.dark else "light") errors = [] page.on("pageerror", lambda e: errors.append(str(e))) page.goto(args.url) expect(page.locator("#health .dot.ok")).to_be_visible(timeout=10_000) expect(page.locator(".empty")).to_be_visible() box = page.locator("#composer textarea") box.fill(args.prompt) t0 = time.time() box.press("Enter") # user bubble shows up and the input clears expect(page.locator(".msg.user")).to_have_count(1) expect(box).to_have_value("") expect(page.locator(".empty")).to_be_hidden() # tokens stream in (plain spans) ... page.wait_for_selector(".streaming span", timeout=60_000) first = time.time() - t0 # ... then get replaced by rendered Markdown page.wait_for_selector(".msg.assistant .markdown", timeout=180_000) total = time.time() - t0 page.wait_for_timeout(300) assert page.locator(".msg.assistant [sse-connect]").count() == 0, "SSE element not cleaned up" if "numbered list" in args.prompt: assert page.locator(".msg.assistant ol li").count() >= 3, "expected a numbered list" if "code" in args.prompt: assert page.locator(".msg.assistant pre code").count() >= 1, "expected a code block" page.screenshot(path=f"{args.out}/screenshot{'-dark' if args.dark else ''}.png") # reload: history persists for the session page.reload() expect(page.locator(".msg")).to_have_count(2) # new chat clears it page.get_by_role("button", name="New chat").click() expect(page.locator(".msg")).to_have_count(0) expect(page.locator(".empty")).to_be_visible() browser.close() if errors: print("JS errors:", errors) sys.exit(1) print(f"OK: first token {first:.1f}s, full reply {total:.1f}s")