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/test_sidebar_scroll_measured.py | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_sidebar_scroll_measured.py (limited to 'packages/meshbay-hub/tests/test_sidebar_scroll_measured.py') diff --git a/packages/meshbay-hub/tests/test_sidebar_scroll_measured.py b/packages/meshbay-hub/tests/test_sidebar_scroll_measured.py new file mode 100644 index 0000000..d2adaf1 --- /dev/null +++ b/packages/meshbay-hub/tests/test_sidebar_scroll_measured.py @@ -0,0 +1,68 @@ +""" +A sidebar with many groups scrolls on its own, on a phone and on a desktop. + +Measured, not read: twelve groups beside a page long enough to scroll, in +Chrome, at 390 and 1280 px. The list must scroll, its last group and the legal +link must be reachable, and reaching an end must not hand the scroll on to the +page behind — which it did, measured before the fix (`overscroll-behavior: +auto`). + +What no headless measurement shows is a phone's address bar: an iframe has no +dynamic toolbar, so `100vh` and `100dvh` agree here. That half is held by +reading the stylesheet, the weaker evidence, and is the known behaviour of +mobile browsers: `100vh` is the height with the bar hidden. +""" + +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +HARNESS = Path(__file__).parent / "harness" / "sidebar_scroll_probe.py" +STYLE = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" / "style.css" + + +@pytest.fixture(scope="module") +def measured(): + if shutil.which("google-chrome") is None: + pytest.skip("Chrome is not available") + proc = subprocess.run([sys.executable, str(HARNESS)], + capture_output=True, text=True, timeout=90) + data = json.loads(proc.stdout.strip().splitlines()[-1]) + assert "error" not in data, f"probe failed: {proc.stdout}{proc.stderr}" + return data + + +@pytest.mark.parametrize("width", ["390", "1280"]) +def test_the_list_scrolls_inside_the_sidebar(measured, width): + m = measured[width] + assert m["overflowY"] == "auto" + assert m["scrollHeight"] > m["clientHeight"], "twelve groups should overflow" + assert m["sidebarBottom"] <= m["viewportHeight"], "the sidebar runs off the window" + + +@pytest.mark.parametrize("width", ["390", "1280"]) +def test_the_end_of_the_list_can_be_reached(measured, width): + m = measured[width] + assert m["lastGroupBottom"] <= m["sidebarBottom"] + assert m["legalBottom"] <= m["sidebarBottom"] + 1 + + +@pytest.mark.parametrize("width", ["390", "1280"]) +def test_reaching_an_end_does_not_scroll_the_page(measured, width): + assert measured[width]["overscrollBehaviorY"] == "contain" + + +def test_the_height_follows_the_visible_viewport_with_a_fallback(): + css = STYLE.read_text(encoding="utf-8") + rules = [m.group(1) for m in re.finditer(r"\n\s*\.sidebar \{(.*?)\}", css, re.S)] + assert len(rules) == 2, "expected the desktop and the phone .sidebar rules" + for body in rules: + heights = re.findall(r"height:\s*calc\((100d?vh)", body) + assert heights == ["100vh", "100dvh"], ( + f"a .sidebar rule sizes itself with {heights}: 100dvh is what keeps the " + "foot of the list above a phone's address bar, after a 100vh fallback") -- cgit v1.2.3