aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_playlist_ui.py
blob: c449dc7d2b7214f970ab6db0a30db782c43f23b8 (plain) (blame)
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"] == []