aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_node_page_width_measured.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-02 10:15:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-02 10:15:56 +0200
commite08bb408cf9963066f212028e69e22c46ff1cb54 (patch)
tree272813441d5a4615c1d8a7b677993fa9b48f9db7 /packages/meshbay-hub/tests/test_node_page_width_measured.py
parent9f6797bfc4d8b55373cc6a957969f03443b5b29d (diff)
downloadmeshbay-e08bb408cf9963066f212028e69e22c46ff1cb54.tar.gz
fix(hub): the Node page takes the width of a settings page
`.node-page` carried `max-width: 700px` of its own while Settings, Profile and the create-group wizard take `.main`'s. The audit tab is a six-column table — timestamp, event, user, IP, group, detail — with every fixed-shape column set `white-space: nowrap` so an IP is never clipped, so at 700px it scrolled sideways inside `.node-table-scroll` with a couple of hundred pixels of `.main` empty beside it. Measured at 1024: 596px of table box where the page had 856px to give. The rule goes; the class stays as the anchor for the assertions. `.node-group` and `.settings-section` are already the same rule twice over (same background, border, radius, padding), so the two pages now line up card for card. test_node_page_width_measured.py: the Node page and a Settings page are the same width at every width from 320 up, their cards the same rectangle, the audit table no longer wider than its scroller at 1024, and — the narrow case being the design rather than a regression — the table still scrolling inside its own box on a phone without pushing the document sideways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
Diffstat (limited to 'packages/meshbay-hub/tests/test_node_page_width_measured.py')
-rw-r--r--packages/meshbay-hub/tests/test_node_page_width_measured.py174
1 files changed, 174 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_node_page_width_measured.py b/packages/meshbay-hub/tests/test_node_page_width_measured.py
new file mode 100644
index 0000000..ebf07ab
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_node_page_width_measured.py
@@ -0,0 +1,174 @@
+"""
+The Node page is as wide as every other settings-shaped page.
+
+`.node-page` carried `max-width: 700px` of its own while Settings, Profile and
+the create-group wizard take `.main`'s width. The Node page's audit tab is a
+six-column table — timestamp, event, user, IP, group, detail — with every
+fixed-shape column set `white-space: nowrap` so an IP is never clipped, so at
+700px it scrolled sideways inside `.node-table-scroll` while a couple of
+hundred pixels of `.main` sat empty beside it.
+
+The rule is gone and the class stays, as the anchor for these assertions: the
+next narrowing has to get past them.
+
+Rectangles rather than declarations, in the manner `test_layout_measured.py`
+established — `max-width: 700px` in a rule says nothing about what the table
+inside actually gets, which is the thing that was wrong.
+"""
+
+import json
+import shutil
+import subprocess
+import textwrap
+from pathlib import Path
+
+import pytest
+
+HARNESS = Path(__file__).parent / "harness" / "layout_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")
+
+AUDIT_ROWS = [
+ ("02/09/2026 01:14:07", "member_join", "someone", "203.0.113.7",
+ "A Group Name", "role=member via invite"),
+ ("02/09/2026 01:12:55", "gek_rotate", "an-operator", "198.51.100.42",
+ "Another Group", "previous key retired, 3 members rewrapped"),
+ ("02/09/2026 00:58:31", "upload_reject", "a-third-person", "192.0.2.198",
+ "A Group Name", "quarantine full"),
+]
+
+
+def _audit_table():
+ head = "".join(f"<th>{c}</th>" for c in
+ ("Time", "Event", "User", "IP", "Group", "Detail"))
+ body = "".join("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>"
+ for row in AUDIT_ROWS)
+ return f"""
+ <div class="node-table-scroll">
+ <table class="node-table node-table-audit">
+ <thead><tr>{head}</tr></thead>
+ <tbody>{body}</tbody>
+ </table>
+ </div>"""
+
+
+# Both pages as the SPA renders them: a `.main` inside the `.layout`, holding
+# either `page-content node-page` (node-page.js) or the bare `<div>` that
+# settings-page.js and profile-page.js return. No sidebar in either, so the
+# only thing that can differ between them is the page's own width.
+FRAGMENT = textwrap.dedent(f"""
+ <div class="layout">
+ <main class="main">
+ <div id="node" class="page-content node-page">
+ <h2>Node</h2>
+ <div class="node-group">
+ <h3 class="settings-heading">Audit</h3>
+ {_audit_table()}
+ </div>
+ </div>
+ </main>
+ </div>
+ <div class="layout">
+ <main class="main">
+ <div id="settings">
+ <h2>Settings</h2>
+ <div class="settings-section">
+ <h3 class="settings-heading">Downloads</h3>
+ <p class="page-message">A line of settings text.</p>
+ </div>
+ </div>
+ </main>
+ </div>
+ """)
+
+WIDTHS = [320, 360, 412, 768, 1024]
+SELECTORS = ["#node.node-page", "#settings", "#node .node-table-scroll",
+ "#node .node-table-audit", "#node .node-group",
+ "#settings .settings-section"]
+
+
+@pytest.fixture(scope="module")
+def measured(tmp_path_factory):
+ """One browser for every width — launching one apiece cost three minutes."""
+ fragment = tmp_path_factory.mktemp("nodewidth") / "fragment.html"
+ fragment.write_text(FRAGMENT)
+ proc = subprocess.run(
+ ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS),
+ str(fragment), *SELECTORS],
+ 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
+
+
+def _box(measured, width, selector):
+ box = measured[str(width)]["boxes"][selector]
+ assert box is not None, f"{selector} did not render at {width} px"
+ return box
+
+
+@pytest.mark.parametrize("width", WIDTHS)
+def test_the_node_page_is_the_width_of_a_settings_page(measured, width):
+ """The request itself. Same `.main`, same page, same width."""
+ node = _box(measured, width, "#node.node-page")["width"]
+ settings = _box(measured, width, "#settings")["width"]
+ assert node == settings, (
+ f"at {width} px the Node page is {node} px wide against the Settings "
+ f"page's {settings} px")
+
+
+@pytest.mark.parametrize("width", WIDTHS)
+def test_a_node_card_lines_up_with_a_settings_card(measured, width):
+ """`.node-group` and `.settings-section` are the same rule twice over —
+ same background, border, radius, padding. They should also be the same
+ rectangle, or the two pages read as different designs."""
+ node = _box(measured, width, "#node .node-group")
+ settings = _box(measured, width, "#settings .settings-section")
+ assert (node["left"], node["width"]) == (settings["left"], settings["width"]), (
+ f"at {width} px a node card is {node['width']} px at x={node['left']} "
+ f"and a settings card {settings['width']} px at x={settings['left']}")
+
+
+def test_the_audit_table_stops_scrolling_sideways_on_a_desktop(measured):
+ """What the width was actually for.
+
+ Six columns, five of them `white-space: nowrap`, in a 700px page: the
+ table was wider than the scroller it sat in, so reading an audit line
+ meant dragging it sideways — with empty page beside it the whole time.
+
+ Measured at 1024px, where `.main` reaches its own 960px cap and the old
+ rule was the only thing standing between the table and the room it needed.
+ Below about 800px the viewport was already the tighter constraint, and the
+ table scrolls there whatever this page is allowed — see the phone case.
+ """
+ table = _box(measured, 1024, "#node .node-table-audit")["width"]
+ scroller = _box(measured, 1024, "#node .node-table-scroll")["width"]
+ assert table <= scroller, (
+ f"the audit table is {table} px inside a {scroller} px scroller — it "
+ "still has to be dragged sideways on a desktop")
+ # `max-width: 700px` left roughly 596px here, after `.main`'s 32px padding
+ # either side and the card's own 20px.
+ assert scroller > 700, (
+ f"the audit table has {scroller} px, which is less room than a page "
+ "capped at 700px would have to give it — the cap is back")
+
+
+@pytest.mark.parametrize("width", [320, 360, 412])
+def test_the_audit_table_still_scrolls_itself_on_a_phone(measured, width):
+ """The narrow case is not a regression, it is the design.
+
+ Six nowrap columns will never fit a phone. `.node-table-scroll` is what
+ keeps that contained: the table scrolls inside its own box and the page
+ does not grow sideways around it.
+ """
+ r = measured[str(width)]
+ assert r["docScrollW"] <= r["viewport"]["w"], (
+ f"the document scrolls to {r['docScrollW']} px on a {width} px screen "
+ "— the audit table is pushing the page wider instead of scrolling")
+ page = _box(measured, width, "#node.node-page")
+ assert page["offRight"] == 0, (
+ f"{page['offRight']} px of the Node page is off a {width} px screen")