Go + Templ + HTMX app that checks the weather (Open-Meteo, MET Norway fallback), scores run/ride/walk with plain rules, and has a local open-weight model (Gemma 4 E2B via any OpenAI-compatible server) write the plan. Routes are loops from your door (BRouter) plus signposted OpenStreetMap routes (Overpass), with GPX export.
69 lines
2.7 KiB
Python
69 lines
2.7 KiB
Python
# /// script
|
|
# requires-python = ">=3.11"
|
|
# dependencies = ["playwright"]
|
|
# ///
|
|
"""Browser end-to-end test: taps "📍 Here" (with a faked GPS fix), waits for
|
|
the coach's streamed advice and the route map, asks a follow-up, and saves
|
|
screenshots.
|
|
|
|
uv run scripts/e2e.py [--url http://127.0.0.1:3000] [--dark] [--mobile]
|
|
"""
|
|
import argparse, sys
|
|
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("--mobile", action="store_true")
|
|
p.add_argument("--lat", type=float, default=51.0543)
|
|
p.add_argument("--lon", type=float, default=3.7174)
|
|
args = p.parse_args()
|
|
|
|
name = "screenshot" + ("-mobile" if args.mobile else "") + ("-dark" if args.dark else "")
|
|
viewport = {"width": 400, "height": 860} if args.mobile else {"width": 1000, "height": 900}
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch(channel=args.channel, headless=True)
|
|
ctx = browser.new_context(
|
|
viewport=viewport,
|
|
color_scheme="dark" if args.dark else "light",
|
|
geolocation={"latitude": args.lat, "longitude": args.lon},
|
|
permissions=["geolocation"],
|
|
device_scale_factor=2,
|
|
)
|
|
page = ctx.new_page()
|
|
errors = []
|
|
page.on("pageerror", lambda e: errors.append(str(e)))
|
|
page.goto(args.url)
|
|
page.evaluate("localStorage.clear()")
|
|
page.reload()
|
|
|
|
expect(page.locator("#health .dot.ok")).to_be_visible(timeout=10_000)
|
|
page.get_by_text("🔥 Hard").click()
|
|
page.get_by_text("📍 Here").click() # locates and submits
|
|
|
|
expect(page.locator(".weather")).to_be_visible(timeout=20_000)
|
|
expect(page.locator(".score.picked")).to_be_visible()
|
|
expect(page.locator("#thread .markdown")).to_be_visible(timeout=180_000)
|
|
if page.locator(".score.indoor.picked").count() == 0:
|
|
expect(page.locator(".routes .map.leaflet-container")).to_be_visible(timeout=60_000)
|
|
page.wait_for_timeout(1500) # tiles
|
|
|
|
page.screenshot(path=f"{args.out}/{name}.png", full_page=True)
|
|
|
|
page.get_by_text("⏱️ Only 30 min").click()
|
|
expect(page.locator("#thread .markdown")).to_have_count(2, timeout=180_000)
|
|
replies = page.locator("#thread .markdown").all_inner_texts()
|
|
print("--- plan ---\n" + replies[0] + "\n--- follow-up ---\n" + replies[1])
|
|
|
|
page.reload() # the plan survives a reload
|
|
expect(page.locator("#thread .markdown")).to_have_count(2)
|
|
|
|
browser.close()
|
|
if errors:
|
|
print("page errors:", errors, file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f"ok: {args.out}/{name}.png")
|