aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_welcome_layout_measured.py142
1 files changed, 142 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_welcome_layout_measured.py b/packages/meshbay-hub/tests/test_welcome_layout_measured.py
new file mode 100644
index 0000000..da72f34
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_welcome_layout_measured.py
@@ -0,0 +1,142 @@
+"""
+The sign-in page with the welcome text beside it, measured in a browser.
+
+Two columns on a desktop, one on a phone with the form first, and nothing wider
+than the screen at any width. The text is the English catalogue's, at its real
+length: a fixture shorter than the real copy measures the fixture.
+
+The markup is `LoginPage` and `WelcomePitch` as `auth-page.js` renders them,
+inside the shell `app.js` puts every page in. What is under test is the
+stylesheet, which is served as it ships.
+"""
+
+import json
+import re
+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")
+
+
+def _en(key: str) -> str:
+ source = (STATIC / "locales" / "en.js").read_text(encoding="utf-8")
+ m = re.search(rf"^\s*'{re.escape(key)}': '([^']*)',$", source, re.M)
+ assert m, f"{key} is missing from en.js"
+ return m.group(1)
+
+
+def _items(*keys: str) -> str:
+ return "".join(f"<li><span>{_en(k)}</span></li>" for k in keys)
+
+
+def _page() -> str:
+ li = _items
+ return textwrap.dedent(f"""
+ <nav class="nav"><div class="nav-left"><a class="nav-brand" href="#/">MeshBay</a></div>
+ <div class="nav-right"><button class="nav-btn">Login</button></div></nav>
+ <div class="layout"><main class="main"><div class="page-center"><div class="welcome">
+ <div class="card login-card"><h2>Login</h2>
+ <form><input type="text" placeholder="Username" />
+ <input type="password" placeholder="Password" /><button>Login</button></form>
+ <div class="login-footer">No account? <a href="#/register">Register</a></div>
+ <div class="login-footer"><a href="#/reset">Forgot your passphrase?</a></div>
+ </div>
+ <section class="welcome-pitch">
+ <h1 class="welcome-title">{_en('welcome.title')}</h1>
+ <p class="welcome-lead">{_en('welcome.lead')}</p>
+ <ul class="welcome-apps">{li('welcome.app_chat', 'welcome.app_photos',
+ 'welcome.app_media', 'welcome.app_video', 'welcome.app_music')}</ul>
+ <p>{_en('welcome.groups')}</p>
+ <p class="welcome-e2e"><span>{_en('welcome.e2e')}</span></p>
+ <h2 class="welcome-h">{_en('welcome.uses_title')}</h2>
+ <ul class="welcome-uses">{li('welcome.use_chat', 'welcome.use_photos',
+ 'welcome.use_media', 'welcome.use_apps')}</ul>
+ <div class="welcome-hub"><h2 class="welcome-h">{_en('welcome.hub_title')}</h2>
+ <p>{_en('welcome.hub_body')}</p>
+ <p class="welcome-hub-never">{_en('welcome.hub_never')}</p>
+ <ul class="welcome-never">{li('welcome.never_transit', 'welcome.never_stored',
+ 'welcome.never_e2e')}</ul>
+ <p class="welcome-hub-free">{_en('welcome.hub_free')}</p></div>
+ <div class="welcome-links"><a class="welcome-cta" href="#">{_en('welcome.download')}</a>
+ <a class="welcome-legal" href="#">{_en('welcome.legal')}</a></div>
+ </section>
+ </div></div></main></div>
+ """)
+
+
+PHONES = [320, 360, 412]
+DESKTOPS = [1100, 1440]
+WIDTHS = PHONES + [768] + DESKTOPS
+SELECTORS = [".welcome", ".login-card", ".welcome-pitch"]
+
+
+@pytest.fixture(scope="module")
+def measured(tmp_path_factory):
+ fragment = tmp_path_factory.mktemp("welcome") / "fragment.html"
+ fragment.write_text(_page(), encoding="utf-8")
+ 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
+ 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_nothing_is_wider_than_the_screen(measured, width):
+ """A document wider than the screen unpins every sticky header on Android."""
+ assert measured[str(width)]["docScrollW"] <= width, (
+ f"the page is {measured[str(width)]['docScrollW']} px wide on a {width} px screen")
+ for sel in SELECTORS:
+ box = _box(measured, width, sel)
+ assert box["offLeft"] == 0 and box["offRight"] == 0, f"{sel} hangs off at {width} px"
+
+
+@pytest.mark.parametrize("width", PHONES + [768])
+def test_a_narrow_screen_shows_the_form_first(measured, width):
+ card, pitch = _box(measured, width, ".login-card"), _box(measured, width, ".welcome-pitch")
+ assert pitch["top"] >= card["top"] + card["height"], (
+ f"at {width} px the welcome text is not below the sign-in form")
+
+
+@pytest.mark.parametrize("width", DESKTOPS)
+def test_a_desktop_puts_the_form_right_of_the_text(measured, width):
+ """Text on the left, the form on the right with room between them, and
+ the form up by the title rather than centred on a column far taller."""
+ card, pitch = _box(measured, width, ".login-card"), _box(measured, width, ".welcome-pitch")
+ gap = card["left"] - (pitch["left"] + pitch["width"])
+ assert gap >= 80, f"at {width} px the form is {gap} px right of the text"
+ assert pitch["width"] >= 480, f"the text column is {pitch['width']} px at {width} px"
+ assert 0 <= card["top"] - pitch["top"] <= 80, (
+ f"at {width} px the form starts {card['top'] - pitch['top']} px below the text")
+
+
+def test_the_pair_is_centred_in_the_window(measured):
+ """Signed out there is no sidebar, and `.main` is capped at 960 px — so
+ before `.page-center` lifted the cap, a centred form sat centred in the
+ left 960 px of the window: x=290 in 1440."""
+ # Against the document's width, not the window's: the probe's frame is
+ # shorter than the page, so a vertical scrollbar takes its share.
+ box = _box(measured, 1440, ".welcome")
+ middle = measured["1440"]["docScrollW"] / 2
+ centre = box["left"] + box["width"] / 2
+ assert abs(centre - middle) <= 2, f"the page is centred on x={centre}, not {middle}"