""" The playlist menus, pressed in a real browser. The store is covered against a stubbed node, and the merge and the sealing by their own tests. None of that reaches the part a person touches: whether the toolbar button opens a menu at all, whether naming a playlist works (Electron has no `prompt` — it *throws*, which is how the Files toolbar's New folder button came to do nothing), and whether "add to playlist" on a cover puts the right tracks in the right playlist. The probe renders the shipped `GroupPage`, `MusicPlayerBar` and playlist menus and presses the real controls. Labels come back in whatever locale the browser picked, so what is asserted is shape and behaviour — counts, order, and what ended up in the store — rather than English strings. """ import json import shutil import subprocess from pathlib import Path import pytest HARNESS = Path(__file__).parent / "harness" / "playlist_ui_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 / "playlist-menu.js").exists(), reason="Chrome or the SPA sources are not available") @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_one_button_opens_the_five_playlist_verbs_in_order(steps): """Load, create, delete, remove a track — the order asked for — and Sync now, which has to live somewhere and this is the only playlist surface.""" assert len(steps["toolbar menu"]["items"]) == 5 def test_a_playlist_is_named_in_a_field_and_not_a_prompt(steps): """`window.prompt` throws in Electron and does not return null, so it is banned outright (`test_no_prompt_in_the_spa.py`). Anything that needs typed input needs a field, and this is the one that does.""" s = steps["created"] assert s["modalGone"] is True, "the modal stayed open, so nothing was saved" assert [p["name"] for p in s["lists"]] == ["Soirée"] assert s["lists"][0]["count"] == 0 def test_the_cover_menu_carries_the_queue_verbs_and_add_to_playlist(steps): assert len(steps["cover menu"]["items"]) == 4 def test_favourites_is_offered_first_before_it_has_ever_been_used(steps): """The reserved playlist is materialised on first use, so the submenu has it on a fresh account — and has it first, as the design promises.""" items = steps["add-to submenu"]["items"] submenu = items[4:] assert len(submenu) == 3, submenu assert submenu[1] == "Soirée" def test_adding_an_album_from_its_cover_puts_its_tracks_in_the_playlist(steps): s = steps["added to the playlist"] assert s["lists"] == [{"name": "Soirée", "count": 3}] assert s["note"], "nothing said it had happened" def test_loading_a_playlist_replaces_the_queue_with_its_tracks(steps): """Below `onPlayQueue` a playlist and an album are indistinguishable, which is why auto-advance, shuffle and prefetch are unchanged by construction.""" assert steps["loaded into the queue"]["play"] == ["A2-t1", "A2-t2", "A2-t3"] def test_the_tracklist_submenu_is_two_levels_and_fetched_when_expanded(steps): """As asked: the playlist, then its tracks. The second level is read from IndexedDB when it is opened — building it eagerly would read every playlist's tracks to draw a menu nobody may open.""" items = steps["the tracklist submenu"]["items"] assert "A2-t1" in items and "A2-t3" in items assert items.index("A2-t1") > 0 # The tracks sit under their playlist, between it and the next top-level # item — expanded in place rather than in a flyout. assert items[-1] == steps["toolbar menu"]["items"][-1] def test_removing_a_track_removes_that_one(steps): s = steps["track removed"] assert s["lists"] == [{"name": "Soirée", "count": 2}] assert s["tracks"] == ["A2-t1", "A2-t3"], "the wrong track was removed" def test_deleting_a_playlist_asks_first(steps): """A deletion is a tombstone: there is nothing in the interface that undoes it. `confirm` and not a component — Electron implements it and a dozen places in this SPA already use it.""" s = steps["deleted"] assert s["asked"] is True, "a playlist was deleted without asking" assert s["lists"] == []