diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_welcome_layout_measured.py | 142 |
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}" |