aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_menu_scroll.py
blob: b56a0531487195e4f0ceaa60b438e9a06595faef (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
134
135
136
137
138
139
140
141
142
"""
A pop-up menu taller than the window can be scrolled, and scrolled all the way.

Two defects, one symptom — "the last track cannot be reached" — and they had to
be fixed in that order, because the first hides the second.

`.ctx-menu` is `overflow-y: auto` under a `max-height`, and a playlist's
tracklist expands *inside* it — sixty tracks is three times the panel's height,
so scrolling is the only way to reach most of them. Both ways of scrolling it
closed it instead: the wheel, and dragging its own scrollbar. Reported against
"remove a track", where the menu vanished before a track could be clicked.

`scroll` does not bubble, so the listener that dismisses the menu on a page
scroll is on the capture phase — and capture is equally what made it hear the
panel's own scrolling on the way down.

Measured, not read. `playlist_ui_probe.py` drives the same menu and cannot see
this: it reaches every row with `.click()`, which scrolls nothing at all.

The last two cases are the ones that must keep closing the menu, and they are
why the fix is a filter on the event's origin rather than a removed listener.

The second defect is where the panel ends. `max-height: calc(100vh - 16px)`
says how tall it may be and nothing about where its bottom lands, so a panel
opened 300px down the window ran 300px past the bottom of it. It scrolled — but
its last rows scrolled into a part of itself that is off the screen, which no
further scrolling brings back. `menu.js` measures the room left below where it
put the panel. The flip case is measured too, because the condition deciding it
was rewritten in the same breath.
"""

import json
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

HARNESS = Path(__file__).parent / "harness" / "menu_scroll_probe.py"


@pytest.fixture(scope="module")
def cases():
    if shutil.which("google-chrome") is None:
        pytest.skip("Chrome is not available")
    proc = subprocess.run([sys.executable, str(HARNESS)],
                          capture_output=True, text=True, timeout=90)
    data = json.loads(proc.stdout)
    assert "error" not in data, f"probe failed: {proc.stdout}{proc.stderr}"
    return {c["case"]: c for c in data["cases"]}


def test_the_fixture_actually_overflows(cases):
    # A menu that fits measures nothing, and would let every assertion below
    # pass with the fix removed.
    c = cases["the panel is scrollable at all"]
    assert c["overflows"], f"60 rows did not overflow the panel: {c}"


def test_the_wheel_scrolls_the_menu_instead_of_closing_it(cases):
    c = cases["scrolled inside the menu"]
    assert c["stillOpen"], "the menu closed when it was scrolled"
    assert c["scrollTop"] == 200, "the menu did not scroll"


def test_dragging_the_menus_own_scrollbar_does_not_close_it(cases):
    c = cases["pressed the menu scrollbar and dragged"]
    assert c["stillOpen"], "pressing the scrollbar closed the menu"
    assert c["scrollTop"] == 400, "the drag did not scroll"


def test_reaching_the_end_does_not_scroll_the_page_behind(cases):
    # Chained to the page, that overscroll *is* a page scroll, and a page
    # scroll closes the menu on purpose — the same symptom by another door.
    assert cases["the panel is scrollable at all"]["overscrollBehaviorY"] == "contain"


def test_a_press_outside_still_closes_it(cases):
    assert cases["pressed outside the menu"]["closed"]


def test_scrolling_the_page_still_closes_it(cases):
    # Not optional: the menu is `position: fixed` and the media grids scroll
    # under a sticky toolbar, so a menu that survives leaves itself pointing at
    # an album that has moved.
    assert cases["scrolled the page underneath"]["closed"]


def test_a_long_menu_ends_inside_the_window(cases):
    c = cases["a long menu opened partway down the window"]
    assert c["overflows"], f"the fixture measures nothing: {c}"
    assert c["panelBottom"] <= c["viewport"], (
        f"the panel runs {c['panelBottom'] - c['viewport']}px past the bottom "
        f"of the window: {c}")


def test_the_last_row_of_a_long_menu_can_be_reached(cases):
    # Scrolled to the end, the last row has to be *visible*. This is the
    # user-facing sentence, and the one that fails while the panel overruns the
    # window: the rows are there, scrolled into a region nothing can show.
    c = cases["a long menu opened partway down the window"]
    assert c["lastRowLabel"].startswith("Track 60"), c
    assert c["lastRowBottom"] <= c["viewport"], (
        f"the last row sits {c['lastRowBottom'] - c['viewport']}px below the "
        f"window after scrolling to the end: {c}")


def test_it_still_opens_at_the_pointer_when_the_room_is_below(cases):
    # The cure must not be a menu that jumps somewhere else: a list too tall
    # for any placement stays where it was asked for and scrolls.
    c = cases["a long menu opened partway down the window"]
    assert c["panelTop"] == c["openedAt"], c


def test_an_expanded_submenu_stays_inside_the_window(cases):
    c = cases["a submenu expanded inside it"]
    assert c["panelBottom"] > c["collapsedBottom"], (
        f"the submenu did not actually expand: {c}")
    assert c["panelBottom"] <= c["viewport"], c
    assert c["lastRowLabel"].startswith("Track 60"), c
    assert c["lastRowBottom"] <= c["viewport"], c


def test_an_expanded_submenu_is_re_placed_rather_than_left_in_its_slot(cases):
    # The size change happens in `MenuItems`' own state, which the placement
    # effect cannot see on its own — this is what the report upward is for.
    # Without it the panel keeps the height measured for its collapsed self:
    # the menu stays inside the window, and a sixty-track list is threaded
    # through the 192px that were free below where it was opened.
    c = cases["a submenu expanded inside it"]
    assert c["panelHeight"] > c["viewport"] - c["openedAt"], (
        f"the panel is still the size the collapsed menu was given: {c}")


def test_a_short_menu_at_the_bottom_edge_still_flips(cases):
    # No regression on the placement that already worked: a menu that fits
    # above the pointer is drawn there whole, not pinned to the edge and
    # squeezed into the few pixels left below it.
    c = cases["a short menu opened at the bottom edge"]
    assert c["panelBottom"] <= c["openedAt"], c
    assert not c["overflows"], f"a four-item menu should not need scrolling: {c}"