""" 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