"""Browser click-test of the dashboard address popup (view + drag-edit + history/revert), driven by Playwright for Python against a locally-seeded app (see scripts/dev_seed.py). No Node / no MCP. # terminal 1 ./.venv/Scripts/python.exe scripts/dev_seed.py # terminal 2 ./.venv/Scripts/python.exe scripts/browser_smoke.py Prints PASS/FAIL per step and drops screenshots in scripts/_browser_shots/. """ from __future__ import annotations import sys from pathlib import Path from playwright.sync_api import expect, sync_playwright BASE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" TOKEN = sys.argv[2] if len(sys.argv) > 2 else "devtoken" SHOTS = Path(__file__).parent / "_browser_shots" SHOTS.mkdir(exist_ok=True) results: list[tuple[bool, str]] = [] def check(cond: bool, msg: str): results.append((bool(cond), msg)) print(f" {'PASS' if cond else 'FAIL'} {msg}") def run(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 900, "height": 1200}) page.goto(f"{BASE}/dashboard?token={TOKEN}", wait_until="networkidle") # 1) auto-login + nav renders page.wait_for_selector(".navlink", timeout=15000) check(page.is_hidden("#gate"), "signed in (gate hidden)") # 2) go to Leads, a batch row is clickable page.click('.navlink[data-target="leads"]') page.wait_for_selector("#leads a.viewl", timeout=10000) rows = page.locator("#leads a.viewl") check(rows.count() >= 1, f"Leads shows {rows.count()} clickable batch row(s)") # a batch contact reads "Batch: …" check("Batch:" in page.inner_text("#leads"), 'contact shows "Batch: "') # 3) open the popup on the row that has prior history (the 8,600 edit) target = None for i in range(rows.count()): if "144th" in rows.nth(i).inner_text(): target = rows.nth(i) break (target or rows.first).click() page.wait_for_selector("#resultModal.show", timeout=10000) page.wait_for_selector("#modalImgWrap img", timeout=10000) check(page.locator("#modalImgWrap img").count() == 1, "overlay image rendered in popup") check(page.locator("#modalPrice tr").count() >= 1, "price breakdown rows present") area_before = page.inner_text("#modalArea") check("sqft" in area_before, f"lawn area shown ({area_before})") page.screenshot(path=str(SHOTS / "01_popup_open.png")) # 4) drag editor present + a drag changes the area and enables Save check(page.locator("#modalImgWrap svg.lawnsvg .vh").count() >= 4, "drag handles present") handle = page.locator("#modalImgWrap svg.lawnsvg .vh").first box = handle.bounding_box() page.mouse.move(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) page.mouse.down() page.mouse.move(box["x"] - 60, box["y"] - 60, steps=8) # drag the corner outward page.mouse.up() page.wait_for_timeout(200) area_after = page.inner_text("#modalArea") check(area_after != area_before, f"area updates live on drag ({area_before} -> {area_after})") check(not page.locator("#modalSave").is_disabled(), "Save enabled after an edit") page.screenshot(path=str(SHOTS / "02_after_drag.png")) # 5) save persists (server round-trip) → "Saved." page.click("#modalSave") page.wait_for_function("document.getElementById('modalSaveMsg').textContent.includes('Saved')", timeout=10000) check(True, "Save round-trip succeeded (Saved.)") # 6) history + revert try: # two async loadHistory() calls can flicker the box — expect() auto-retries expect(page.locator("#modalHistory")).to_contain_text("Measurement history", timeout=10000) check(True, "history section shown") except Exception: check(False, "history section shown") revs = page.locator("#modalHistory .hrev") check(revs.count() >= 1, f"{revs.count()} revert target(s) available") page.screenshot(path=str(SHOTS / "03_history.png")) area_pre_revert = page.inner_text("#modalArea") revs.first.click() page.wait_for_function( f"document.getElementById('modalArea').textContent !== {area_pre_revert!r}", timeout=10000) check(page.inner_text("#modalArea") != area_pre_revert, f"revert changed the area ({area_pre_revert} -> {page.inner_text('#modalArea')})") # 7) close page.click("#modalClose") page.wait_for_selector("#resultModal", state="hidden", timeout=5000) check(not page.is_visible("#resultModal"), "popup closes with X") page.screenshot(path=str(SHOTS / "04_closed.png")) browser.close() if __name__ == "__main__": try: run() except Exception as exc: # noqa: BLE001 print(f" FAIL harness error: {type(exc).__name__}: {exc}") results.append((False, "harness error")) passed = sum(1 for ok, _ in results if ok) print(f"\n{passed}/{len(results)} checks passed") sys.exit(0 if passed == len(results) and results else 1)