"""HEL-342: static visual shots + computed-style probe (no business data writes).""" from __future__ import annotations import json from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from threading import Thread ROOT = Path(__file__).resolve().parents[1] WEB = ROOT / "web" OUT = ROOT.parent / "hel342-shots" PROBES = [ (".sidebar", ["width", "backgroundColor", "backdropFilter"]), (".brand-logo-day", ["width", "height"]), (".btn-primary", ["backgroundColor", "height", "borderRadius", "color"]), (".stat-card.gold .stat-value, .stat-card .stat-value", ["color", "fontSize", "fontVariantNumeric"]), (".page-head h1", ["fontSize", "fontWeight"]), (".ds-table th", ["fontSize", "color"]), ] def main() -> None: from playwright.sync_api import sync_playwright OUT.mkdir(parents=True, exist_ok=True) handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) Thread(target=httpd.serve_forever, daemon=True).start() base = f"http://127.0.0.1:{httpd.server_address[1]}" report = [] pages = [ ("index", "index.html", None, None), ("login-admin", "login-admin.html", None, None), ("login-company", "login-company.html", None, None), ("admin-dashboard", "admin.html", "dashboard", None), ("admin-flows", "admin.html", "flows", None), ("admin-settings", "admin.html", "settings", None), ("admin-audit", "admin.html", "audit", None), ("admin-reminders", "admin.html", "reminders", None), ("admin-period-audit", "admin.html", "period-audit", None), ("admin-companies", "admin.html", "companies", None), ("admin-pair", "admin.html", "pair", None), ("company-workspace", "company.html", "workspace", None), ("company-transfers", "company.html", "transfers", None), ("company-reconcile", "company.html", "reconcile", None), ("company-upload", "company.html", "upload", None), ] with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) for theme in ("day", "night"): for width, height, tag in ((1440, 900, "1440"), (820, 900, "820"), (390, 844, "390")): if tag != "1440" and theme == "night" and width != 390: continue page = browser.new_page(viewport={"width": width, "height": height}) page.add_init_script( f"() => {{ try {{ localStorage.setItem('jinniu-theme', '{theme}'); }} catch (e) {{}} }}" ) for name, html, view, _ in pages: if tag != "1440" and name not in ("login-admin", "admin-flows", "company-transfers", "admin-dashboard"): continue page.route("**/app.js**", lambda route: route.abort()) page.goto(f"{base}/{html}", wait_until="domcontentloaded") page.evaluate( """(args) => { document.documentElement.classList.add('v-fusion'); document.documentElement.setAttribute('data-theme', args.theme); if (args.view) { document.querySelectorAll('.app-view').forEach((el) => { el.classList.toggle('is-active', el.dataset.page === args.view); }); } }""", {"theme": theme, "view": view}, ) page.wait_for_timeout(80) shot = OUT / f"{name}-{theme}-{tag}.png" page.screenshot(path=str(shot), full_page=False) if tag == "1440" and name in ("login-admin", "admin-dashboard", "admin-flows"): probe = page.evaluate( """(sels) => sels.map(([sel, props]) => { const el = document.querySelector(sel); if (!el) return { sel, missing: true }; const cs = getComputedStyle(el); const out = { sel }; props.forEach((p) => { out[p] = cs[p]; }); const box = el.getBoundingClientRect(); out.box = { w: Math.round(box.width), h: Math.round(box.height) }; return out; })""", PROBES, ) overflow = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) report.append({"page": name, "theme": theme, "overflow": overflow, "probe": probe}) page.close() # 390 drawer page = browser.new_page(viewport={"width": 390, "height": 844}) page.add_init_script("() => { try { localStorage.setItem('jinniu-theme', 'day'); } catch (e) {} }") page.route("**/app.js**", lambda route: route.abort()) page.goto(f"{base}/company.html", wait_until="domcontentloaded") page.evaluate( """() => { document.documentElement.classList.add('v-fusion'); document.querySelectorAll('.app-view').forEach((el) => { el.classList.toggle('is-active', el.dataset.page === 'transfers'); }); const d = document.getElementById('transferEvidenceDrawer'); if (d) d.classList.add('is-open'); }""" ) page.screenshot(path=str(OUT / "company-transfers-drawer-day-390.png"), full_page=False) page.close() browser.close() httpd.shutdown() (OUT / "probe.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") print(f"wrote {OUT} ({len(list(OUT.glob('*.png')))} png)") if __name__ == "__main__": main()