diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_layout_measured.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_layout_measured.py | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py new file mode 100644 index 0000000..91f1ed0 --- /dev/null +++ b/packages/meshbay-hub/tests/test_layout_measured.py @@ -0,0 +1,146 @@ +""" +Layouts measured in a browser instead of read out of the stylesheet. + +`test_layout_responsive.py` says, in its own docstring, that a layout cannot be +measured here because there is no browser in the suite. There is one now — +Chrome is what the video work has been verified against — and the difference +matters: `width: 330px` in a rule tells you nothing about whether the thing +lands on the screen. That depends on where its anchor sits, which depends on +everything to its right. + +The transfers panel is the case that proved it. 330 px wide, anchored to the +right edge of its button — but that button is not at the right edge of the +screen, the bell and the user menu come after it. Measured before the fix: + + 320 px viewport -> panel at -138..192, 138 px off the left + 360 px -> -98..232 + 412 px -> -46..284 + +What is cut off is the left-hand side, which is where the file names are, so +what was left on screen was a strip of progress bars belonging to nothing — +reported as "on mobile you only see the right-hand side, without the content". + +Every assertion here is a rectangle, not a declaration. +""" + +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") + +# The nav as `Nav()` renders it: the transfers widget is not last — the bell and +# the user menu follow it, which is the whole reason the panel hangs off. +NAV = textwrap.dedent(""" + <nav class="nav"> + <div class="nav-left"> + <button class="nav-hamburger">☰</button> + <a class="nav-brand" href="#/">MeshBay</a> + </div> + <div class="nav-right"> + <div class="transfer-wrap"> + <button class="nav-notif transfer-btn">↓</button> + <div class="transfer-panel"> + <div class="transfer-head">Transfers<button class="btn-secondary">Clear</button></div> + <div class="transfer-item"> + <div class="transfer-line"> + <span class="transfer-kind">↓</span> + <span class="transfer-name">S03E01. Salt and Sea, Fire and Blood.mp4</span> + <button class="transfer-cancel">✕</button> + </div> + <div class="dl-progress"><div class="dl-fill" style="width:42%"></div></div> + <div class="transfer-meta"><span>210 MB / 493 MB</span><span>3.1 MB/s</span></div> + </div> + </div> + </div> + <a class="nav-notif" href="#/">🔔</a> + <div class="user-menu"><button class="nav-btn">someone</button></div> + </div> + </nav> +""") + + +WIDTHS = [320, 360, 412, 768, 1024] +SELECTORS = [".transfer-panel", ".transfer-name"] + + +@pytest.fixture(scope="module") +def measured(tmp_path_factory): + """One browser for every width, because launching one apiece cost the + suite three minutes.""" + fragment = tmp_path_factory.mktemp("layout") / "fragment.html" + fragment.write_text(NAV) + 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}" + for w in WIDTHS: + assert out[str(w)]["viewport"]["w"] == w, ( + f"asked for {w} px and measured at {out[str(w)]['viewport']['w']}") + return out + + +def _box(measured, width: int, selector: str) -> dict: + return measured[str(width)]["boxes"][selector] + + +@pytest.mark.parametrize("width", [320, 360, 412]) +def test_the_transfers_panel_fits_a_phone(measured, width): + """The reported defect, as a rectangle.""" + box = _box(measured, width, ".transfer-panel") + assert box is not None, "the panel did not render" + assert box["offLeft"] == 0, ( + f"{box['offLeft']} px of the panel is off the left of a {width} px " + "screen — and the left is where the file names are") + assert box["offRight"] == 0, ( + f"{box['offRight']} px of the panel is off the right of a {width} px screen") + + +@pytest.mark.parametrize("width", [320, 360]) +def test_the_file_name_gets_room_to_be_read(measured, width): + """Fitting on screen is not the same as being legible. + + A panel could satisfy the test above by being narrow enough to show + nothing. The name is the one part a viewer needs. + """ + box = _box(measured, width, ".transfer-name") + assert box is not None and box["width"] >= 180, ( + f"the file name has {box['width'] if box else 0} px on a {width} px " + "screen, which is not enough to tell two downloads apart") + + +def test_the_desktop_panel_is_untouched(measured): + """The fix is a media query, and it must stay inside it.""" + box = _box(measured, 1024, ".transfer-panel") + assert box["width"] == 330, ( + f"the desktop panel is now {box['width']} px — the mobile rule has " + "escaped its breakpoint") + assert box["offLeft"] == 0 and box["offRight"] == 0 + + +def test_it_does_not_stretch_across_a_tablet(measured): + """Pinned to both edges, a panel would be 750 px wide at 768.""" + box = _box(measured, 768, ".transfer-panel") + assert box["width"] <= 440, ( + f"{box['width']} px of panel on a tablet, which is a list of two " + "filenames stretched over most of the screen") + + +@pytest.mark.parametrize("width", [320, 360, 412]) +def test_the_page_does_not_scroll_sideways(measured, width): + """The other half of "it fits": nothing pushed the document wider.""" + r = measured[str(width)] + assert r["docScrollW"] <= r["viewport"]["w"], ( + f"the document scrolls to {r['docScrollW']} px on a {width} px screen") |