Initial commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
dump-rendered.py — Open a URL (typically the local dev server) and dump its DOM
|
||||
using the same __CLONE_DUMP_COMPUTED__ walker the capture pipeline uses for the
|
||||
source. This produces a directly-comparable structural snapshot the validate
|
||||
agent can diff against the captured original.
|
||||
|
||||
Usage:
|
||||
dump-rendered.py --url http://localhost:3000/ --output workspace/rendered/1280-step-00.json \
|
||||
--viewport 1280 --scroll-y 0
|
||||
|
||||
Dependencies: playwright.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
|
||||
except ImportError:
|
||||
print(json.dumps({"error": "Missing dependencies. pip install playwright && playwright install chromium"}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_init_hooks(hooks_dir: Path) -> list[str]:
|
||||
sources: list[str] = []
|
||||
# We only need computed-styles + section-scan for rendered-DOM purposes.
|
||||
for name in ("bootstrap.js", "computed-styles.js", "section-scan.js"):
|
||||
p = hooks_dir / name
|
||||
if p.exists():
|
||||
sources.append(p.read_text())
|
||||
return sources
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--output", required=True, help="Path to write the rendered DOM JSON")
|
||||
parser.add_argument("--viewport", type=int, default=1280)
|
||||
parser.add_argument("--viewport-height", type=int, default=None, help="Default: 16:9 of viewport width")
|
||||
parser.add_argument("--scroll-y", type=int, default=0)
|
||||
parser.add_argument("--reduce-motion", action="store_true")
|
||||
parser.add_argument("--settle-ms", type=int, default=800)
|
||||
args = parser.parse_args()
|
||||
|
||||
output = Path(args.output).resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
height = args.viewport_height or round(args.viewport * 9 / 16)
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
init_sources = load_init_hooks(script_dir / "init-hooks")
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"])
|
||||
ctx = browser.new_context(viewport={"width": args.viewport, "height": height})
|
||||
for src in init_sources:
|
||||
ctx.add_init_script(src)
|
||||
page = ctx.new_page()
|
||||
|
||||
try:
|
||||
page.goto(args.url, wait_until="networkidle", timeout=30_000)
|
||||
except PWTimeout:
|
||||
page.goto(args.url, wait_until="domcontentloaded", timeout=30_000)
|
||||
|
||||
if args.reduce_motion:
|
||||
try:
|
||||
page.evaluate("document.documentElement.classList.add('reduce-motion')")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
page.evaluate("() => document.fonts.ready")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
page.wait_for_function("window.__CLONE_READY__ === true", timeout=5_000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if args.scroll_y:
|
||||
page.evaluate(f"window.scrollTo({{ top: {args.scroll_y}, behavior: 'instant' }})")
|
||||
|
||||
time.sleep(args.settle_ms / 1000.0)
|
||||
|
||||
snapshot = page.evaluate(
|
||||
"() => (typeof window.__CLONE_DUMP_COMPUTED__ === 'function') ? window.__CLONE_DUMP_COMPUTED__() : null"
|
||||
)
|
||||
sections = page.evaluate(
|
||||
"() => (typeof window.__CLONE_LIST_SECTIONS__ === 'function') ? window.__CLONE_LIST_SECTIONS__() : []"
|
||||
) or []
|
||||
|
||||
if snapshot is None:
|
||||
raise SystemExit("dump-rendered: __CLONE_DUMP_COMPUTED__ unavailable — init hooks failed to load")
|
||||
|
||||
output.write_text(json.dumps(snapshot, separators=(",", ":")))
|
||||
sections_path = output.with_name(output.stem + "-sections.json")
|
||||
sections_path.write_text(json.dumps(sections, indent=2))
|
||||
|
||||
page.close()
|
||||
ctx.close()
|
||||
browser.close()
|
||||
|
||||
print(json.dumps({
|
||||
"status": "ok",
|
||||
"output": str(output),
|
||||
"sections_path": str(sections_path),
|
||||
"section_count": len(sections),
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user