aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-15 17:01:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-15 17:01:06 +0200
commit459fc93e98f23e326c2fa77fe86ba74c2bae77f0 (patch)
treef506a127cc3e0779c4d8cf9f27fe659d81bb787e /packages/meshbay-hub/tests
parentfce0942cb7d81dbdbae70e50b0164fead7e8bc49 (diff)
downloadmeshbay-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')
-rwxr-xr-xpackages/meshbay-hub/tests/harness/sticky_header_probe.py12
-rw-r--r--packages/meshbay-hub/tests/test_hook_ordering.py2
-rw-r--r--packages/meshbay-hub/tests/test_media_pager.py71
-rw-r--r--packages/meshbay-hub/tests/test_sticky_header.py13
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py2
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