""" A pop-up menu taller than the window can be scrolled without dismissing itself. `.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. """ 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"]