diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-25 16:31:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-25 16:31:52 +0200 |
| commit | 8bb94a39609f57be2c486f579849eebb04606808 (patch) | |
| tree | 46d5553f514619d8f1f33af6eb32a6de314a03d0 /packages/meshbay-hub/tests | |
| parent | 10c58792c7591accdb84c92201f77843ec39ee7b (diff) | |
| download | meshbay-8bb94a39609f57be2c486f579849eebb04606808.tar.gz | |
feat(hub): friendlier welcome page, link previews, robots.txt and favicon
Welcome page: privacy said once, a three-step "how it works", a
documentation box, download (green) and legal links under the sign-in
form, on a dark gradient backdrop covering the whole page.
Link previews: Open Graph tags in the app shell, rendered for
identity.id, with the square icon as image.
robots.txt, favicon and touch icon served at the origin root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_site_basics.py | 81 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_welcome_layout_measured.py | 48 |
2 files changed, 117 insertions, 12 deletions
diff --git a/packages/meshbay-hub/tests/test_site_basics.py b/packages/meshbay-hub/tests/test_site_basics.py new file mode 100644 index 0000000..a3da39a --- /dev/null +++ b/packages/meshbay-hub/tests/test_site_basics.py @@ -0,0 +1,81 @@ +""" +The files every website is expected to have at the root of its origin. + +They live in the hub's static directory, which is mounted at "/", so every hub +serves them — meshbay.org included, because its Caddyfile hands the hub every +path the public site does not name. +""" + +import re +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient +from meshbay_hub.app import create_app + +CADDYFILE = Path(__file__).resolve().parents[3] / "packaging" / "caddy" / "meshbay.org.Caddyfile" + + +@pytest.fixture(scope="module") +def client(): + return TestClient(create_app()) + + +def test_robots_keeps_crawlers_out_of_the_signed_in_views(client): + r = client.get("/robots.txt") + assert r.status_code == 200 + assert r.headers["content-type"].startswith("text/plain") + rules = re.findall(r"^Disallow:\s*(\S+)", r.text, re.M) + assert "/app/" in rules and "/v1/" in rules + # A crawler rendering the sign-in page needs its scripts and stylesheet. + assert not any(rule in ("/", "/a/", "/app") for rule in rules), rules + + +@pytest.mark.parametrize("path, magic", [ + ("/favicon.ico", b"\x00\x00\x01\x00"), + ("/apple-touch-icon.png", b"\x89PNG"), +]) +def test_the_icons_are_served_from_the_root(client, path, magic): + """Browsers ask for both at the root whether or not a page links them.""" + r = client.get(path) + assert r.status_code == 200 + assert r.content.startswith(magic), f"{path} is not the image it claims to be" + + +@pytest.mark.skipif(not CADDYFILE.exists(), reason="no Caddyfile in this tree") +@pytest.mark.parametrize("path", ["/robots.txt", "/favicon.ico", "/apple-touch-icon.png"]) +def test_meshbay_org_sends_them_to_the_hub(path): + """The public site owns only the paths its matcher names; a file claimed + there would be looked for in /srv/meshbay/site and 404.""" + m = re.search(r"^\s*@site path (.+)$", CADDYFILE.read_text(), re.M) + assert m, "the @site matcher is gone" + assert path not in m.group(1).split() + + +def _og(html: str, prop: str) -> str | None: + m = re.search(rf'<meta property="og:{prop}" content="([^"]*)">', html) + return m and m.group(1) + + +def test_a_link_to_the_hub_previews_with_the_logo(): + """Messengers draw a link from og:title and og:image, and resolve only an + absolute image URL — so it names the hub's public name, and the file is + one the hub serves.""" + from meshbay_hub.config import load_config + cfg = load_config() + cfg.identity.id = "hub.example.org" + client = TestClient(create_app(cfg)) + for path in ("/", "/app"): + html = client.get(path).text + assert _og(html, "title") == "MeshBay" + assert _og(html, "image") == "https://hub.example.org/og-image.jpg", path + image = client.get("/og-image.jpg") + assert image.status_code == 200 and image.content.startswith(b"\xff\xd8") + assert len(image.content) < 300_000, "too heavy for some messengers to fetch" + + +def test_the_preview_description_fits_in_a_preview(): + """A preview shows a line or two and cuts the rest; a cut sentence says + nothing.""" + from meshbay_hub.api.webapp import PREVIEW_DESCRIPTION + assert len(PREVIEW_DESCRIPTION) <= 60 diff --git a/packages/meshbay-hub/tests/test_welcome_layout_measured.py b/packages/meshbay-hub/tests/test_welcome_layout_measured.py index da72f34..5a77cb9 100644 --- a/packages/meshbay-hub/tests/test_welcome_layout_measured.py +++ b/packages/meshbay-hub/tests/test_welcome_layout_measured.py @@ -40,34 +40,46 @@ def _items(*keys: str) -> str: def _page() -> str: li = _items + # The source row carries the full URL rather than the host name it shows: + # an unbreakable string longer than the real one. + docs = "".join( + f'<li><span class="welcome-docs-label">{_en(label)}</span>' + f'<span class="welcome-docs-links">{links}</span></li>' + for label, links in [ + ("welcome.docs_source", "https://git.meshbay.org/meshbay.git/about/"), + ("welcome.docs_user", f"{_en('welcome.docs_quickstart')} · {_en('welcome.docs_userguide')}"), + ("welcome.docs_devel", f"{_en('welcome.docs_design')} · {_en('welcome.docs_protocol')}"), + ]) 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> + <div class="welcome-side"><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> + <div class="welcome-links"><a class="welcome-cta" href="#">{_en('welcome.download')}</a> + <a class="welcome-legal" href="#">{_en('welcome.legal')}</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.how_title')}</h2> + <ol class="welcome-steps">{li('welcome.step_home', 'welcome.step_anywhere', + 'welcome.step_share')}</ol> <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> + <div class="welcome-private"><span class="welcome-private-icon"></span><div> + <h2 class="welcome-private-title">{_en('welcome.private_title')}</h2> + <p>{_en('welcome.private_body')}</p> + <ul class="welcome-badges">{li('welcome.badge_free', 'welcome.badge_open', + 'welcome.badge_no_ads', 'welcome.badge_no_tracking')}</ul></div></div> + <div class="welcome-docs"><h2 class="welcome-h">{_en('welcome.docs_title')}</h2> + <ul>{docs}</ul></div> </section> </div></div></main></div> """) @@ -76,7 +88,8 @@ def _page() -> str: PHONES = [320, 360, 412] DESKTOPS = [1100, 1440] WIDTHS = PHONES + [768] + DESKTOPS -SELECTORS = [".welcome", ".login-card", ".welcome-pitch"] +SELECTORS = [".welcome", ".login-card", ".welcome-pitch", ".welcome-steps", ".welcome-docs", + ".welcome-links"] @pytest.fixture(scope="module") @@ -140,3 +153,14 @@ def test_the_pair_is_centred_in_the_window(measured): 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}" + + +@pytest.mark.parametrize("width", WIDTHS) +def test_the_download_and_legal_links_sit_under_the_form(measured, width): + card, links = _box(measured, width, ".login-card"), _box(measured, width, ".welcome-links") + assert links["top"] >= card["top"] + card["height"], ( + f"at {width} px the links are not below the sign-in form") + assert links["top"] - (card["top"] + card["height"]) <= 40, ( + f"at {width} px the links are far below the form") + assert abs(links["left"] - card["left"]) <= 1 and abs(links["width"] - card["width"]) <= 1, ( + f"at {width} px the links are not aligned with the form") |