#!/usr/bin/env python3
"""
Can the reader scroll up in the chat, and does the panel sit still when nobody
is touching it?
`scroll_probe.py` answers "is the document taller than the window". This one
mounts **the real `ChatPanel`** — the shipped module, not a model of it — in a
browser, drives it the way a person does, and reports where the list ends up.
It exists because the defect it was written for is invisible to every other kind
of test here. `fit()` set the panel's height, read the document's overflow back
and subtracted it, so the document alternately did and did not overflow the
window; the page scrollbar appeared and vanished with it, `visualViewport` fired
`resize` at each pass, and `fit()` is bound to that event. It therefore re-ran
about 120 times a second for the life of the panel, re-pinning the list to the
bottom every time — which undid each attempt to scroll up *inside the same
frame*, before the `scroll` event that would have recorded it was delivered. The
source reads as correct: every pin is guarded by "only if the reader is at the
bottom", and the reader never got to stop being at the bottom.
chat_scroll_probe.py
Prints JSON: an `idle` block (viewport-resize firings on a page nobody touches)
and a `steps` list (scroll position after each stage of a reading session).
"""
import http.server
import json
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"
PORT = 8747
RECORDS = []
socketserver.TCPServer.allow_reuse_address = True
# A bare document fires a couple of these while it settles. Anything above this
# is the panel driving itself.
IDLE_RESIZE_CEILING = 20
PAGE = r"""
a group
a description
"""
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 == "/":
body, ctype = PAGE.encode(), "text/html; charset=utf-8"
else:
path = (STATIC / self.path.lstrip("/")).resolve()
if not str(path).startswith(str(STATIC)) or not path.is_file():
self.send_response(404)
self.end_headers()
return
body = path.read_bytes()
ctype = ("text/css" if path.suffix == ".css"
else "text/javascript" if path.suffix == ".js"
else "application/octet-stream")
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
# ignore_cleanup_errors: Chrome's children (zygote, renderer, gpu)
# outlive terminate() on the parent by a moment and go on writing into
# the profile. rmtree then walks a directory that gains a file between
# its readdir and its rmdir and raises "Directory not empty" -- which
# failed the probe, which failed every test in the file, intermittently
# and for a reason nowhere near the chat code they were testing. A few
# bytes left in a throwaway profile are harmless; failing the run is not.
with tempfile.TemporaryDirectory(
ignore_cleanup_errors=True) as profile:
# Real time, not `--virtual-time-budget`: the defect is a feedback
# loop between layout and an event, and a virtual clock does not
# run it.
proc = subprocess.Popen(
["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
f"--user-data-dir={profile}", "--window-size=1100,800",
f"http://127.0.0.1:{PORT}/"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(300):
if RECORDS:
break
time.sleep(0.1)
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
if not RECORDS:
print(json.dumps({"error": "no measurement"}), file=sys.stderr)
return 1
print(json.dumps(RECORDS[0], indent=1))
return 0
if __name__ == "__main__":
raise SystemExit(main())