#!/usr/bin/env python3 """layout-probe — find controls that render but cannot be clicked. WHY THIS EXISTS. On 2026-09-21 the operator reported "release button covers delete button". Both controls were `position:absolute` on the same corner of a kept card, and `release` was the later sibling, so it painted over the × with 30x22 px of overlap on a 30px button. `elementFromPoint` at the ×'s centre returned the release form: the × was 100% unclickable from the day it shipped. Nothing in the test suite could have caught it. The markup was correct, the route was correct, the CSS was individually valid. OCCLUSION IS A PROPERTY OF THE RENDERED LAYOUT, and the only instrument that sees it is a browser. This is the second control shipped inert in two days — the first was a reveal button whose handler Jinja discarded. Both were reported by the operator, both would have taken ten seconds to catch by looking at the page. USAGE scripts/layout-probe.py [URL ...] Exits 0 if every control is hittable, 1 if any is occluded. No arguments probes the booth index and every booth linked from it. """ import sys from playwright.sync_api import sync_playwright DEFAULT = "http://10.100.10.50:8090/" # Does a click at this element's centre actually reach it? HIT = """(el) => { // ⚠ Use getClientRects()[0], NOT getBoundingClientRect(). For an INLINE // element that WRAPS, the bounding rect is the union of its line boxes and // its geometric centre can land in the gutter between lines — on the parent, // not on the element. The third version of this probe reported three zip // links as OCCLUDED for exactly that reason: long booth names wrapped the // link, and `elementFromPoint` correctly returned the parent .sub div. Real // geometry, wrong question. Per-line rects ask the right one. const rects = el.getClientRects(); const r = rects.length ? rects[0] : el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return 'ZERO-SIZE'; const x = r.x + r.width / 2, y = r.y + r.height / 2; if (x < 0 || y < 0 || x > innerWidth || y > innerHeight) return 'OFF-SCREEN'; const top = document.elementFromPoint(x, y); if (!top) return 'OFF-SCREEN'; // `top.contains(el)` is NOT a hit and must never be added back. An ANCESTOR // receiving the click is precisely what occlusion looks like when the // overlay is a parent or a parent's ::after, and an ancestor trivially // contains its descendant — that clause made version one report OK for a // real overlay. A DESCENDANT receiving it is fine: resolves to the // img and the anchor still gets the click. return (el === top || el.contains(top)) ? 'OK' : 'OCCLUDED'; }""" def probe(page, url: str) -> list[str]: bad = [] page.goto(url, wait_until="networkidle") # Hover every card first: these UIs reveal controls on hover, and an # opacity-0 control still occupies layout and still occludes. for card in page.locator("article.card").all(): try: card.hover(timeout=1500) except Exception: pass for el in page.locator("button, a.dl-link, a.thumb").all(): try: # ⚠ elementFromPoint is VIEWPORT-relative. Without scrolling first, # every control below the fold reports OCCLUDED and the probe # drowns its real findings in noise — which is what the second # version did on a page with sixteen kept booths. el.scroll_into_view_if_needed(timeout=1500) verdict = el.evaluate(HIT) except Exception: continue if verdict in ("OCCLUDED", "ZERO-SIZE"): label = (el.get_attribute("aria-label") or el.get_attribute("title") or (el.text_content() or "").strip()[:30] or "?") bad.append(f"{url} {verdict:<10} {label}") return bad def main(argv: list[str]) -> int: urls = argv[1:] or [DEFAULT] failures = [] with sync_playwright() as pw: b = pw.chromium.launch() pg = b.new_page(viewport={"width": 1400, "height": 900}) for u in urls: failures += probe(pg, u) b.close() if failures: print("UNCLICKABLE CONTROLS:") for f in failures: print(" ", f) return 1 print(f"all controls hittable across {len(urls)} page(s)") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))