summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_music_grid.py
blob: e457474dc240081278189b4c1cb024b2212b4098 (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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""
How much of the Music grid is whitespace.

Every artist used to get a grid container of their own, so an artist with a
single album got a heading and one cover on a row that fits five — and a real
library is mostly single-album artists: a compilation bought once, one album of
somebody's, a soundtrack. Consecutive singles share one grid now, in place, so
the page stays in artist order and a run of them fills a row.

The layout is `auto-fill` over a width nothing declares, so reading the source
proves nothing: this measures the rendered rectangles in a browser and groups
the covers by the row they actually landed on.

Every artist keeps their name at heading weight, pooled or not. Dropping it for
pooled covers was the first version of this and it was wrong: scrolling then
alternates between artists written large and artists written small, and the eye
has to work out which kind of row it is looking at. The heading moves into the
cell instead of going away.

Measured on this fixture — three artists with several albums, twelve with one —
the page went from **4208px to 1895px**, and a walk of it reaches all 21 covers
instead of 9.
"""

import json
import shutil
import subprocess
from pathlib import Path

import pytest

HARNESS = Path(__file__).parent / "harness" / "music_grid_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 / "music-app.js").exists(),
    reason="Chrome or the SPA sources are not available")

# The fixture's artists, in the order the grid sorts them.
MULTI = ["Alpha", "Bravo", "Kilo"]
# One of them is deliberately far too long for a cell, to catch a heading that
# wraps and pushes its own cover out of line with the rest of its row.
LONG = "Foxtrot Un Nom Vraiment Tres Long Qui Ne Tient Pas"
SINGLES_BEFORE_KILO = ["Charlie", "Delta", "Echo", LONG, "Golf",
                       "Hotel", "India", "Juliett"]
SINGLES_AFTER_KILO = ["Lima", "Mike", "November", "Oscar"]


@pytest.fixture(scope="module")
def grid():
    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 out["steps"][0]


def test_every_cover_is_reachable_by_scrolling_the_page(grid):
    """Tiles mount on intersection, so a page tall enough is a page whose last
    covers a reader has to work for. Nine of twenty-one were reachable in the
    same walk before."""
    assert grid["cards"] == 21


def test_every_artist_is_named_in_the_same_type_pooled_or_not(grid):
    """The correction. Three headings sit above a grid, twelve sit inside a
    pooled cell — but all fifteen are `music-artist-heading`, so scrolling does
    not alternate between artists written large and artists written small."""
    assert grid["headings"] == MULTI, "a section heading went missing"
    assert grid["poolHeadings"] == 12, (
        "a single-album artist lost the name above their cover")


def test_a_long_artist_name_does_not_push_its_cover_out_of_line(grid):
    """A pooled heading is clipped to one line. The grid stretches a cell to the
    row height; it does not align what is inside the cell, so a heading that
    wrapped would drop its own cover below its neighbours'."""
    heights = {c["headingH"] for row in grid["cells"] for c in row
               if c["headingH"] is not None}
    assert len(heights) == 1, f"pooled headings are not all one line: {heights}"

    for row in grid["cells"]:
        tops = {c["top"] for c in row}
        assert len(tops) == 1, f"covers on one row start at different heights: {row}"

    assert any(c["heading"] == LONG for row in grid["cells"] for c in row), (
        "the over-long name is not in the fixture any more, so this checks nothing")


def test_single_album_artists_share_a_row(grid):
    """The point of the change. A row carrying several different artists cannot
    happen while each has a grid container to itself."""
    shared = [r for r in grid["rows"] if len(set(r)) > 1]
    assert shared, f"no row carries more than one artist: {grid['rows']}"
    assert max(len(r) for r in shared) >= 4, (
        f"the widest shared row holds {max(len(r) for r in shared)} covers; at "
        f"1100px the grid fits five")


def test_an_artist_with_several_albums_keeps_their_own_rows(grid):
    """Unchanged, and deliberately: a heading earns its line when there is more
    than one cover under it."""
    for artist in MULTI:
        rows = [r for r in grid["rows"] if artist in r]
        assert rows, f"{artist} drew no row"
        for r in rows:
            assert set(r) == {artist}, (
                f"{artist} shares a row with {set(r) - {artist}}")


def test_pooling_happens_in_place_and_keeps_the_page_in_artist_order(grid):
    """Swept into a bin at the end, a run of singles would be easier to build
    and would break the one thing a reader scrolling relies on. The run either
    side of a multi-album artist is two pools, not one."""
    assert grid["pools"] == 2

    order = [a for row in grid["rows"] for a in row]
    first = {a: order.index(a) for a in order}
    assert first["Charlie"] > first["Bravo"]
    assert first["Kilo"] > first["Juliett"]
    assert first["Lima"] > first["Kilo"]


def test_the_singles_are_pooled_with_their_neighbours_not_with_each_other(grid):
    """Every single-album artist appears, and none of them alone on a row —
    except where a row simply ran out of them."""
    order = [a for row in grid["rows"] for a in row]
    for artist in SINGLES_BEFORE_KILO + SINGLES_AFTER_KILO:
        assert artist in order, f"{artist} is missing from the grid"
    lonely = [r for r in grid["rows"] if len(r) == 1]
    assert not lonely, f"a cover is alone on its row: {lonely}"