From acb0d666540dacb092f596dada25822d1d0466f0 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 15 Sep 2026 02:18:38 +0200 Subject: fix(ui): sidebar stays above a phone's address bar and does not scroll the page Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm --- .../tests/harness/sidebar_scroll_probe.py | 147 +++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py (limited to 'packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py') diff --git a/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py b/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py new file mode 100644 index 0000000..0324367 --- /dev/null +++ b/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Measure the sidebar holding many groups, at a phone and a desktop size. + +Renders the real style.css with the sidebar as `Sidebar()` draws it — twelve +groups, the drawer open on the phone — beside a page long enough to scroll, and +reports what a person would meet: whether the list scrolls, whether its last +group and the legal link can be reached, and whether reaching the end hands the +scroll on to the page. + +What this cannot show is the address bar of a real phone: an iframe has no +dynamic toolbar, so `100vh` and `100dvh` measure the same here. + + sidebar_scroll_probe.py +""" +import http.server +import json +import shutil +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 = 8744 +SIZES = [(390, 740), (1280, 800)] + +GROUPS = "".join( + f'' + f'' + f'A Shared Library Number {i}' + f'@owner_account' + for i in range(1, 13)) + +FRAGMENT = f""" + +
+ +
a long page
+
""" + +PAGE = """ +
""" + +RECORDS = [] + + +def main() -> int: + 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 = (PAGE % {"sizes": json.dumps(SIZES), + "fragment": json.dumps(FRAGMENT)}).encode() + ctype = "text/html; charset=utf-8" + elif self.path == "/style.css": + body = (STATIC / "style.css").read_bytes() + ctype = "text/css" + else: + self.send_response(404) + self.end_headers() + return + 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) + + class S(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + + srv = S(("127.0.0.1", PORT), H) + threading.Thread(target=srv.serve_forever, daemon=True).start() + profile = tempfile.mkdtemp(prefix="chrome-sidebar-") + chrome = subprocess.Popen([ + "google-chrome", "--headless=new", "--no-sandbox", + "--window-size=1400,1700", "--user-data-dir=" + profile, + f"http://127.0.0.1:{PORT}/", + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + deadline = time.time() + 45 + while time.time() < deadline and not RECORDS: + time.sleep(0.2) + chrome.terminate() + try: + chrome.wait(timeout=10) + except subprocess.TimeoutExpired: + chrome.kill() + chrome.wait() + shutil.rmtree(profile, ignore_errors=True) + srv.shutdown() + if not RECORDS: + print(json.dumps({"error": "no measurement"})) + return 1 + print(json.dumps(RECORDS[0])) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.2.3