From cd2e89f5f5cccdb116db4fcb82d00b6325972782 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 18 Aug 2026 17:46:54 +0200 Subject: fix: the chat tab no longer scrolls, and a group is listed or invite-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **The chat tab was 8px too tall, at every window size.** The panel is sized from JS to `viewport - top - 16`, which puts its bottom 16px above the fold — but it sits inside `.main`, which adds 24px of padding below it. Eight pixels of document past the window, whatever the window. Measured at 700, 900 and 1200: `scrollHeight` 708, 908, 1208. This is the second one of these — the sign-in card was `.page-center` and `.layout` each reserving `100vh - 52px` — so it is now measured in the suite rather than reasoned about. `tests/harness/scroll_probe.py` renders the real markup against the real stylesheet and **runs the real `fit()` lifted out of `app.js`**: a copy of the formula in a test would go on passing after the original changed, which is exactly the bug being guarded. The fix does not encode 24 anywhere. The first pass runs as before, then the leftover is measured and taken off, so anything added below the panel later is absorbed the same way. Now `scrollHeight == innerHeight` at all three heights, nothing below the fold, and the panel still fills the room it has — that last one has its own test, because shrinking the chat to 240px would satisfy every other assertion here and be useless. The Settings tab was measured too and is **not** a bug: it fits at 1200px and overflows only when its content is genuinely taller than the window. **Group creation asked one question twice.** Visibility and admission were separate selectors that could only ever be set together — picking Public reached over and set the policy — and two of the four combinations are meaningless. The API already refused public+invite with a 422, so the form could build a request that could not succeed. Private+open was accepted and should not have been: a group anyone may join that nobody can find is a listing with the listing removed, since joining goes through the node and there is no link to pass around. So: one selector, "who can join", and the request derives the rest. The API now refuses the other impossible pair as well, with a message that says which way to resolve it. Six locale strings the visibility box owned are deleted rather than left unread in ten files, and the two surviving descriptions now say what each choice means for who can *find* the group — with the word "public" gone from the page, nothing else would have said it, and someone would publish a group without meaning to. 865 tests pass. Co-Authored-By: Claude Opus 5 --- .../meshbay-hub/tests/test_page_does_not_scroll.py | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_page_does_not_scroll.py (limited to 'packages/meshbay-hub/tests/test_page_does_not_scroll.py') diff --git a/packages/meshbay-hub/tests/test_page_does_not_scroll.py b/packages/meshbay-hub/tests/test_page_does_not_scroll.py new file mode 100644 index 0000000..1187a7a --- /dev/null +++ b/packages/meshbay-hub/tests/test_page_does_not_scroll.py @@ -0,0 +1,125 @@ +""" +A page whose content fits the window must not offer a scrollbar. + +Twice now. First the sign-in card: `.layout` and `.page-center` each reserved +`100vh - 52px` and the second sat inside the first's 24px padding, so the +document was 48px too tall at every window size. Then the chat tab: the panel is +sized from JS to `viewport - top - 16`, which puts its bottom 16px above the +fold — but `.main` adds 24px of padding below it, so the document came out +**exactly 8px too tall, at every window size**, which is what "there is always a +scrollbar" means. + +Neither is visible in the stylesheet. Both are one subtraction against another, +in different files, and the only way to see them is to measure the document +against the window — which is what this does, running the real `fit()` lifted +out of `app.js` rather than a copy of it. +""" + +import json +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +HARNESS = Path(__file__).parent / "harness" / "scroll_probe.py" +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" + +pytestmark = pytest.mark.skipif( + shutil.which("google-chrome") is None or not (STATIC / "style.css").exists(), + reason="Chrome or the SPA stylesheet is not available") + +HEIGHTS = [700, 900, 1200] + +NAV_AND_SIDEBAR = """ + +""" + +CHAT_TAB = NAV_AND_SIDEBAR + """ +
+ +
+

a group

+
+ + + +
+
+

hello

+
+
+
+
+""" + +SHORT_PAGE = NAV_AND_SIDEBAR + """ +
+ +
+

a group

+

not much here

+
+
+""" + + +def _measure(fragment: str, tmp_path: Path) -> dict: + path = tmp_path / "fragment.html" + path.write_text(fragment, encoding="utf-8") + proc = subprocess.run( + ["python3", str(HARNESS), str(path), ",".join(str(h) for h in HEIGHTS)], + capture_output=True, text=True, timeout=180) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + out = json.loads(proc.stdout) + assert "error" not in out, f"no measurement: {out}" + return out + + +@pytest.fixture(scope="module") +def chat(tmp_path_factory): + return _measure(CHAT_TAB, tmp_path_factory.mktemp("chat")) + + +@pytest.fixture(scope="module") +def short(tmp_path_factory): + return _measure(SHORT_PAGE, tmp_path_factory.mktemp("short")) + + +@pytest.mark.parametrize("height", HEIGHTS) +def test_the_chat_tab_fits_its_window(chat, height): + r = chat[str(height)] + assert r["overflow"] <= 0, ( + f"the document is {r['overflow']}px taller than the {height}px window — " + f"a scrollbar on the chat tab. Past the fold: {r['past']}") + + +@pytest.mark.parametrize("height", HEIGHTS) +def test_nothing_on_the_chat_tab_hangs_below_the_fold(chat, height): + """The composer is the one that matters: a chat you cannot type in.""" + assert chat[str(height)]["past"] == [] + + +@pytest.mark.parametrize("height", HEIGHTS) +def test_the_chat_panel_uses_the_room_it_has(chat, height): + """The correction must not overshoot. The panel should end just above the + fold, not halfway up the page — a 240px chat in a 1200px window would pass + every assertion above and be useless.""" + panel = chat[str(height)]["panel"] + assert panel, "no chat panel in the measurement" + gap = height - panel["bottom"] + assert 0 <= gap <= 40, ( + f"the panel ends {gap}px above the fold at {height}px") + + +@pytest.mark.parametrize("height", HEIGHTS) +def test_a_short_page_does_not_scroll_either(short, height): + """The control: without this, a chat panel shrunk to nothing would pass.""" + r = short[str(height)] + assert r["overflow"] <= 0, f"{r['overflow']}px of overflow with no content" -- cgit v1.2.3