diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-15 17:01:06 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-15 17:01:06 +0200 |
| commit | 459fc93e98f23e326c2fa77fe86ba74c2bae77f0 (patch) | |
| tree | f506a127cc3e0779c4d8cf9f27fe659d81bb787e /packages/meshbay-hub/tests | |
| parent | fce0942cb7d81dbdbae70e50b0164fead7e8bc49 (diff) | |
| download | meshbay-459fc93e98f23e326c2fa77fe86ba74c2bae77f0.tar.gz | |
feat(hub): Videos and Music list their cards a page at a time
Previous/next arrows in the pinned toolbar, on group pages and in Search.
Page size is an account preference (Settings → Defaults), 50 by default,
10 to 200 in steps of 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
5 files changed, 95 insertions, 5 deletions
diff --git a/packages/meshbay-hub/tests/harness/sticky_header_probe.py b/packages/meshbay-hub/tests/harness/sticky_header_probe.py index 320a2d4..ee1e61b 100755 --- a/packages/meshbay-hub/tests/harness/sticky_header_probe.py +++ b/packages/meshbay-hub/tests/harness/sticky_header_probe.py @@ -185,7 +185,11 @@ for (let i = 0; i < 40; i++) add({ name: `${ARTISTS[i % ARTISTS.length]} au Zenith (2019) 1080p.mkv`, path: 'films', type: 'video', display_title: `${ARTISTS[i % ARTISTS.length]} au Zenith` }); -for (let a = 0; a < 20; a++) +// Forty films and forty albums against a page of thirty, so Videos and Music +// draw their page arrows in the toolbar being measured (pager.js) — a band +// that only ever gets them past fifty would never be measured with them. +const PAGE_SIZE = '30'; +for (let a = 0; a < 40; a++) for (let t = 0; t < 2; t++) add({ name: `${t + 1} track.flac`, path: `musique/${ARTISTS[a % ARTISTS.length]} ${a}/disque ${t}`, @@ -319,7 +323,7 @@ const settle = () => new Promise((r) => render(html`<${GroupPage} groupId="g1" token="t" username="me" userId="u1" group=${{ id: 'g1', name: 'un groupe', owner_username: 'me', is_admin: false }} - userPrefs=${{ default_tab: 'files' }} />`, + userPrefs=${{ default_tab: 'files', media_page_size: PAGE_SIZE }} />`, document.getElementById('root')); } else { render(html`<${SearchPage} token="t" username="me" userId="u1" @@ -329,7 +333,7 @@ const settle = () => new Promise((r) => not. */''} groups=${[{ id: 'g1', name: 'musique_de_la_maison_2024', owner_username: 'cbesson' }]} - userPrefs=${{}} />`, document.getElementById('root')); + userPrefs=${{ media_page_size: PAGE_SIZE }} />`, document.getElementById('root')); // Results only exist once an index has arrived; the view toggle is drawn // with them. const field = await waitFor('.search-bar input'); @@ -377,6 +381,8 @@ const settle = () => new Promise((r) => bottomScrollY: Math.round(scrollY), scrollY: scrolledBy, room: Math.round(room), overflowX: overflowX(), + // Whether the toolbar carried its page arrows while it was measured. + pager: !!document.querySelector('.video-toolbar > .tb-pager'), // Whether the Search page's group column is on screen. It is dropped at // phone widths, where the file name needs every pixel it can get. groupColumn: (() => { diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py index 3e9c968..28ec93e 100644 --- a/packages/meshbay-hub/tests/test_hook_ordering.py +++ b/packages/meshbay-hub/tests/test_hook_ordering.py @@ -35,7 +35,7 @@ APP = STATIC / "app.js" STATIC_FILES = [ "app.js", "group-page.js", "chat-app.js", "files-app.js", "video-player.js", "video-app.js", "music-app.js", "music-player.js", - "photos-app.js", + "photos-app.js", "pager.js", "group-settings.js", # The per-app settings architecture (docs/refactor-groups.md §3). Reached # through the apps.js registry rather than imported by name, so a file diff --git a/packages/meshbay-hub/tests/test_media_pager.py b/packages/meshbay-hub/tests/test_media_pager.py new file mode 100644 index 0000000..a8a0569 --- /dev/null +++ b/packages/meshbay-hub/tests/test_media_pager.py @@ -0,0 +1,71 @@ +""" +Videos and Music show their cards a page at a time (`pager.js`). + +The page size is a hub preference, so two things can go wrong without any +error on screen: a stored value the client misreads, and a key the hub refuses +— the Settings select would snap back and nothing would say why. The functions +are read out of `pager.js` rather than copied, and the key out of both files. +""" + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" +PAGER = ROOT / "static" / "pager.js" +USERS = ROOT / "api" / "users.py" + +pytestmark = pytest.mark.skipif( + shutil.which("node") is None or not PAGER.exists(), + reason="node or the SPA sources are not available") + + +@pytest.fixture(scope="module") +def source(): + text = PAGER.read_text(encoding="utf-8") + consts = re.findall(r"^export const PAGE_SIZE_\w+ = [^;]+;", text, re.M) + funcs = [re.search(rf"^export function {name}\(.*?^\}}", text, re.M | re.S) + for name in ("pageSizeFrom", "pageBounds")] + assert len(consts) == 4 and all(funcs), "pager.js no longer has what this test reads" + return "\n".join(consts + [m.group(0) for m in funcs]).replace("export ", "") + + +def _run(tmp_path, source, expr): + script = tmp_path / "case.js" + script.write_text(f"{source}\nconsole.log(JSON.stringify({expr}));") + out = subprocess.run(["node", str(script)], capture_output=True, text=True, check=True) + return json.loads(out.stdout) + + +@pytest.mark.parametrize("stored, expected", [ + (None, 50), ("", 50), ("30", 30), ("10", 10), ("200", 200), + ("0", 50), ("210", 50), ("35", 50), ("abc", 50), ("-10", 50), +]) +def test_page_size_from_preference(tmp_path, source, stored, expected): + prefs = {} if stored is None else {"media_page_size": stored} + assert _run(tmp_path, source, f"pageSizeFrom({json.dumps(prefs)})") == expected + + +@pytest.mark.parametrize("total, size, page, expected", [ + (0, 50, 0, {"page": 0, "last": 0, "start": 0, "end": 0}), + (120, 50, 0, {"page": 0, "last": 2, "start": 0, "end": 50}), + (120, 50, 2, {"page": 2, "last": 2, "start": 100, "end": 120}), + (100, 50, 1, {"page": 1, "last": 1, "start": 50, "end": 100}), + # The list shrank under the reader: the last page that exists, not an empty one. + (60, 50, 4, {"page": 1, "last": 1, "start": 50, "end": 60}), + (60, 50, -1, {"page": 0, "last": 1, "start": 0, "end": 50}), +]) +def test_page_bounds(tmp_path, source, total, size, page, expected): + assert _run(tmp_path, source, f"pageBounds({total}, {size}, {page})") == expected + + +def test_hub_accepts_the_key_the_client_writes(): + key = re.search(r"^export const PAGE_SIZE_PREF = '([^']+)';", + PAGER.read_text(encoding="utf-8"), re.M).group(1) + allowed = re.search(r"ALLOWED_PREF_KEYS = frozenset\(\[(.*?)\]\)", + USERS.read_text(encoding="utf-8"), re.S).group(1) + assert f'"{key}"' in allowed diff --git a/packages/meshbay-hub/tests/test_sticky_header.py b/packages/meshbay-hub/tests/test_sticky_header.py index 32b8124..741b57f 100644 --- a/packages/meshbay-hub/tests/test_sticky_header.py +++ b/packages/meshbay-hub/tests/test_sticky_header.py @@ -78,6 +78,19 @@ def test_every_view_was_reached(measured): f"expected {EXPECTED_CASES} measurements, got {len(measured)}") +def test_media_toolbars_were_measured_with_their_page_arrows(measured): + """ + Videos and Music put previous/next arrows in the pinned toolbar once a + library is longer than a page (`pager.js`). The fixture sets a page smaller + than its library so every measurement above covers the toolbar with them; + this fails the fixture if that stops being true. + """ + media = [c for c in measured if c["name"].split(" @")[0].endswith(("videos", "music"))] + assert media, "no Videos or Music case was measured" + bare = [c["name"] for c in media if not c.get("pager")] + assert not bare, "measured without page arrows: " + ", ".join(bare) + + def test_the_page_really_scrolled(measured): """ Everything below compares a scrolled page with an unscrolled one. A view diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py index f780073..d13cfd4 100644 --- a/packages/meshbay-hub/tests/test_transport_contracts.py +++ b/packages/meshbay-hub/tests/test_transport_contracts.py @@ -31,7 +31,7 @@ CREATE_GROUP = STATIC / "create-group-page.js" SPLIT_FILES = [APP, GROUP_PAGE, CHAT_APP, STATIC / "files-app.js", STATIC / "video-player.js", STATIC / "video-app.js", STATIC / "music-app.js", STATIC / "music-player.js", - STATIC / "photos-app.js", + STATIC / "photos-app.js", STATIC / "pager.js", STATIC / "group-settings.js", # Same reason as test_hook_ordering's STATIC_FILES: these are # reached through the registry, so leaving one out here means it |