1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
"""
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"] == []
|