aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_music_queue.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_music_queue.py')
-rw-r--r--packages/meshbay-hub/tests/test_music_queue.py115
1 files changed, 115 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_music_queue.py b/packages/meshbay-hub/tests/test_music_queue.py
new file mode 100644
index 0000000..a53047d
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_music_queue.py
@@ -0,0 +1,115 @@
+"""
+Play, play next, add to queue — pressed in a real browser.
+
+`test_queue_ops.py` executes the reducer directly, which is where the index
+arithmetic is proved. It cannot reach any of this: whether a right-click opens
+a menu at all, whether the dots button still works now that a track row is a
+div holding two buttons rather than one big one, and whether `op` survives the
+trip from the menu through the view, the page's wrapper and the shell.
+
+That last one is not hypothetical. Both wrappers took `(tracks, startIndex)`
+and forwarded two arguments, so every "add to queue" in a group arrived at the
+player as a plain play and silently replaced the queue. Nothing about that
+reads as wrong at either end; this probe is what found it.
+
+The probe renders the shipped `GroupPage` and `MusicPlayerBar`, presses the
+real controls, and reads the queue out of the player's own panel.
+"""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+HARNESS = Path(__file__).parent / "harness" / "music_queue_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 / "queue-ops.js").exists(),
+ reason="Chrome or the SPA sources are not available")
+
+ALBUM = {n: [f"A{n}-t1", f"A{n}-t2", f"A{n}-t3"] for n in (1, 2, 3, 4)}
+
+
+@pytest.fixture(scope="module")
+def steps():
+ proc = subprocess.run(["python3", str(HARNESS)],
+ capture_output=True, text=True, timeout=300)
+ assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}"
+ out = json.loads(proc.stdout)
+ assert "error" not in out, out
+ assert not out.get("logs"), f"the page logged: {out['logs']}"
+ return {s["step"]: s for s in out["steps"]}
+
+
+def test_playing_a_track_queues_its_album(steps):
+ """The ordinary path, through a row that is no longer one single button."""
+ s = steps["play a track"]
+ assert s["play"] == ALBUM[1]
+ assert s["playing"] == 1
+ assert s["nowPlaying"] == "A1-t2"
+
+
+def test_right_click_opens_the_three_queue_verbs(steps):
+ """Three items, in the order the menu promises. Their labels are whatever
+ the browser's locale renders, so this counts them rather than reading them
+ — the point is that the menu opened and is not empty."""
+ assert len(steps["append album 2"]["menu"]) == 3
+
+
+def test_add_to_queue_appends_and_leaves_the_playhead_alone(steps):
+ """The bug this file exists for: this used to replace the queue."""
+ s = steps["append album 2"]
+ assert s["play"] == ALBUM[1] + ALBUM[2]
+ assert s["nowPlaying"] == "A1-t2", "appending moved what was playing"
+
+
+def test_play_next_lands_after_the_playing_track(steps):
+ """Reached through the dots button rather than a right-click, so both
+ affordances are exercised."""
+ s = steps["play next album 3"]
+ assert s["play"] == ["A1-t1", "A1-t2"] + ALBUM[3] + ["A1-t3"] + ALBUM[2]
+ assert s["nowPlaying"] == "A1-t2"
+
+
+def test_shuffling_keeps_everything_that_was_queued(steps):
+ on = steps["shuffle on"]
+ assert sorted(on["play"]) == sorted(ALBUM[1] + ALBUM[2] + ALBUM[3])
+ assert on["playing"] == 0 and on["nowPlaying"] == "A1-t2", (
+ "shuffle moved the track that was already playing")
+
+
+def test_shuffling_off_returns_to_the_order_tracks_were_added_in(steps):
+ """Not to the play order "play next" built: unshuffled *is* the order the
+ tracks arrived in, and that is what it has always meant. A track inserted
+ next while shuffled keeps its place only while shuffle is on."""
+ s = steps["shuffle off"]
+ assert s["play"] == ALBUM[1] + ALBUM[2] + ALBUM[3]
+ assert s["nowPlaying"] == "A1-t2"
+
+
+def test_play_all_replaces_everything(steps):
+ s = steps["replace with album 4"]
+ assert s["play"] == ALBUM[4]
+ assert s["playing"] == 0
+
+
+# ── the wrappers, read rather than driven ────────────────────────────────────
+#
+# The probe proves the chain works for the group page. Search mounts the same
+# view through a wrapper of its own, and there is no cheap way to drive that
+# page's index fetch here — so this reads the one thing that broke.
+
+@pytest.mark.parametrize("name", ["group-page.js", "search-page.js"])
+def test_the_music_wrapper_names_the_op_it_forwards(name):
+ """A wrapper that takes two arguments forwards two, and the third is lost
+ without a word. Both of these did exactly that."""
+ src = (STATIC / name).read_text()
+ marker = ("const onPlayQueue = useCallback((tracks, startIndex, op)"
+ if name == "group-page.js"
+ else "const handleMusicPlay = useCallback((tracks, startIndex, op)")
+ assert marker in src, (
+ f"{name}'s music wrapper no longer names `op`; every 'add to queue' "
+ f"and 'play next' reaching it becomes a plain play")