#!/usr/bin/env python3 """ Does the page scroll vertically when it should not? `layout_probe.py` answers "where is this box". This one answers "is the document taller than the window", which is a different question and the one behind two separate reports of a scrollbar that would not go away. The sizing code under test is **read out of `app.js` and run here**, not reimplemented: a copy of the formula living in the test would go on passing after the real one changed, which is the failure mode worth avoiding in a file whose whole purpose is to catch an arithmetic slip. scroll_probe.py [,,...] Reports per viewport height: the window, the document, the difference, every element hanging below the fold, and the chat panel's box if there is one. """ import http.server import json import re import socketserver import subprocess import sys import tempfile import threading import time from pathlib import Path STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static" # fit() is ChatPanel's own viewport-sizing logic, moved to chat-app.js in the # group-page refactor. APP = STATIC / "chat-app.js" PORT = 8736 FRAG = Path(sys.argv[1]).read_text() HEIGHTS = ([int(h) for h in sys.argv[2].split(",")] if len(sys.argv) > 2 else [700, 900, 1200]) def chat_fit_body() -> str: """ The body of the chat panel's `fit()`, lifted from `app.js`. It closes over `el` and two constants, so those are supplied around it; everything between the braces — the second pass included — is the shipped code. A rename breaks this loudly, which is intended: a skipped test here would be worse than a failing one. """ source = APP.read_text(encoding="utf-8") start = source.index(" const fit = () => {") end = source.index("\n };", start) body = source[source.index("{", start) + 1:end] consts = {} for name in ("CHAT_MIN_HEIGHT", "CHAT_BOTTOM_GAP"): line = re.search(rf"^const {name} = (\d+);", source, re.M) assert line, f"{name} is gone or was renamed" consts[name] = line.group(1) return ("(el, window, document) => {" f"const CHAT_MIN_HEIGHT = {consts['CHAT_MIN_HEIGHT']};" f"const CHAT_BOTTOM_GAP = {consts['CHAT_BOTTOM_GAP']};" + body + "}") PAGE = """
""" RECORDS = [] socketserver.TCPServer.allow_reuse_address = True class H(http.server.BaseHTTPRequestHandler): def log_message(self, *a): pass def do_POST(self): RECORDS.append(json.loads( self.rfile.read(int(self.headers["Content-Length"])).decode())) self.send_response(204) self.end_headers() def do_GET(self): if self.path == "/": # A "" inside the fragment would close the inline script # it is embedded in, and the page would measure nothing. body = (PAGE % {"frag": json.dumps(FRAG).replace(" int: with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv: threading.Thread(target=srv.serve_forever, daemon=True).start() with tempfile.TemporaryDirectory() as profile: subprocess.run( ["google-chrome", "--headless", "--disable-gpu", "--no-sandbox", f"--user-data-dir={profile}", "--window-size=1100,1300", "--virtual-time-budget=6000", "--dump-dom", f"http://127.0.0.1:{PORT}/"], capture_output=True, timeout=120) for _ in range(50): if RECORDS: break time.sleep(0.1) print(json.dumps(RECORDS[0] if RECORDS else {"error": "no measurement"}, indent=1)) return 0 if __name__ == "__main__": raise SystemExit(main())