diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 8 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_node_page_width_measured.py | 174 |
2 files changed, 181 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index c33c91a..3d9206d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2467,7 +2467,13 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } /* ── Node management (D5) ────────────────────────────────────────────────── */ -.node-page { max-width: 700px; } +/* Deliberately no width of its own. It used to be `max-width: 700px`, which + put the six-column audit table — timestamp, event, user, IP, group, detail — + into a box narrower than the table, so it scrolled sideways inside the page + while 260px of `.main` sat empty beside it. The Node page is a settings- + shaped page and takes `.main`'s width like Settings, Profile and the + create-group wizard. The class stays as the anchor for the test that says + so. */ .node-group { background: var(--bg-surface); 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") |