from __future__ import annotations import argparse import json import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] def _open_driver(width: int, height: int): from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service options = Options() options.add_argument("--headless=new") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument(f"--window-size={width},{height}") candidates = [ PROJECT_ROOT / "chromedriver", PROJECT_ROOT / "chromedriver.exe", ] last_err: Exception | None = None for path in candidates: if not path.is_file(): continue try: driver = webdriver.Chrome(service=Service(str(path)), options=options) break except Exception as exc: # pragma: no cover - environment dependent last_err = exc else: try: driver = webdriver.Chrome(options=options) except Exception as exc: # pragma: no cover - environment dependent raise RuntimeError(f"selenium init failed: {last_err or exc}") from exc driver.execute_cdp_cmd( "Emulation.setDeviceMetricsOverride", { "width": width, "height": height, "deviceScaleFactor": 1, "mobile": False, }, ) return driver def check_final_html(path: Path, *, width: int, height: int, tolerance: int) -> dict: driver = _open_driver(width, height) try: driver.get(path.resolve().as_uri()) result = driver.execute_script( r""" const de = document.documentElement; const body = document.body; const slide = document.querySelector('.slide'); const slideRect = slide ? slide.getBoundingClientRect() : null; const measure = (el) => ({ clientWidth: el.clientWidth, clientHeight: el.clientHeight, scrollWidth: el.scrollWidth, scrollHeight: el.scrollHeight, }); return { viewport: { innerWidth: window.innerWidth, innerHeight: window.innerHeight, }, documentElement: measure(de), body: measure(body), slide: slideRect ? { x: slideRect.x, y: slideRect.y, width: slideRect.width, height: slideRect.height, right: slideRect.right, bottom: slideRect.bottom, } : null, }; """ ) finally: driver.quit() failures: list[str] = [] vp = result["viewport"] doc = result["documentElement"] body = result["body"] slide = result.get("slide") if doc["scrollWidth"] > vp["innerWidth"] + tolerance: failures.append( f"document horizontal scroll: {doc['scrollWidth']} > {vp['innerWidth']}" ) if doc["scrollHeight"] > vp["innerHeight"] + tolerance: failures.append( f"document vertical scroll: {doc['scrollHeight']} > {vp['innerHeight']}" ) if body["scrollWidth"] > vp["innerWidth"] + tolerance: failures.append(f"body horizontal scroll: {body['scrollWidth']} > {vp['innerWidth']}") if body["scrollHeight"] > vp["innerHeight"] + tolerance: failures.append(f"body vertical scroll: {body['scrollHeight']} > {vp['innerHeight']}") if not slide: failures.append(".slide not found") else: if abs(slide["width"] - width) > tolerance or abs(slide["height"] - height) > tolerance: failures.append( f"slide size is {slide['width']}x{slide['height']}, expected {width}x{height}" ) if slide["x"] < -tolerance or slide["y"] < -tolerance: failures.append(f"slide origin out of viewport: x={slide['x']}, y={slide['y']}") if slide["right"] > vp["innerWidth"] + tolerance: failures.append(f"slide right edge exceeds viewport: {slide['right']} > {vp['innerWidth']}") if slide["bottom"] > vp["innerHeight"] + tolerance: failures.append(f"slide bottom edge exceeds viewport: {slide['bottom']} > {vp['innerHeight']}") return { "path": str(path), "passed": not failures, "failures": failures, "metrics": result, } def _run_id_to_final_html(run_id: str) -> Path: return PROJECT_ROOT / "data" / "runs" / run_id / "phase_z2" / "final.html" def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("run_ids", nargs="+") parser.add_argument("--width", type=int, default=1280) parser.add_argument("--height", type=int, default=720) parser.add_argument("--tolerance", type=int, default=1) parser.add_argument("--write-report", type=Path) args = parser.parse_args(argv) reports = [] exit_code = 0 for run_id in args.run_ids: path = _run_id_to_final_html(run_id) if not path.is_file(): report = { "path": str(path), "passed": False, "failures": ["final.html not found"], "metrics": None, } else: report = check_final_html( path, width=args.width, height=args.height, tolerance=args.tolerance, ) reports.append({"run_id": run_id, **report}) status = "PASS" if report["passed"] else "FAIL" print(f"{run_id}: {status}") for failure in report["failures"]: print(f" - {failure}") if not report["passed"]: exit_code = 1 if args.write_report: args.write_report.parent.mkdir(parents=True, exist_ok=True) args.write_report.write_text( json.dumps({"reports": reports}, ensure_ascii=False, indent=2), encoding="utf-8", ) return exit_code if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))