"""The station pane: paging, the position readout and the scrollbar.""" from __future__ import annotations import re import pytest from beefy_player.app import BeefyPlayerApp from beefy_player.widgets.station_list import StationList def app_for(provider: str, *, offline: bool = True) -> BeefyPlayerApp: return BeefyPlayerApp(provider=provider, autoplay=False, offline=offline, audio=False) def test_thumb_geometry() -> None: # nothing to scroll: the whole track is the thumb assert StationList.thumb(10, 10, 0) == set(range(10)) # top of a long list top = StationList.thumb(10, 100, 0) assert 0 in top and len(top) >= 1 # bottom of the same list ends at the last row bottom = StationList.thumb(10, 100, 90) assert 9 in bottom # mid-list sits between the two middle = StationList.thumb(10, 100, 45) assert min(middle) > min(top) and max(middle) < max(bottom) @pytest.mark.asyncio async def test_paging_moves_a_screen_and_back() -> None: app = app_for("somafm") async with app.run_test(size=(100, 26)) as pilot: await pilot.pause() pane = app.query_one(StationList) page = pane.size.height - 1 start = app.index await pilot.press("pagedown") await pilot.pause() assert app.index == min(start + page, len(app.view) - 1) await pilot.press("pageup") await pilot.pause() assert app.index == start # the bottom of the list clamps instead of wrapping await pilot.press("shift+g", "pagedown") await pilot.pause() assert app.index == len(app.view) - 1 @pytest.mark.asyncio async def test_the_title_reads_out_the_position() -> None: app = app_for("somafm") async with app.run_test(size=(100, 26)) as pilot: await pilot.pause() pane = app.query_one(StationList) assert re.fullmatch(r"STATIONS · \d+/\d+", pane.border_title) await pilot.press("down") await pilot.pause() first, total = pane.border_title.split(" · ")[1].split("/") assert int(total) == len(app.view) assert int(first) == app.index + 1 @pytest.mark.asyncio async def test_the_scrollbar_appears_only_when_the_list_overflows() -> None: app = app_for("somafm") async with app.run_test(size=(100, 26)) as pilot: await pilot.pause() pane = app.query_one(StationList) rows = [line.plain for line in pane.render().renderables] assert rows[0].startswith("╭") and rows[-1].startswith("╰") content = rows[1:-1] # thumb only, in the frame grey: blank cells elsewhere, no rail assert all(row[-2] in "▐ " for row in content) assert any(row[-2] == "▐" for row in content) app = app_for("rinse") # three stations, no scrolling async with app.run_test(size=(100, 26)) as pilot: await pilot.pause() pane = app.query_one(StationList) rows = [line.plain for line in pane.render().renderables] assert rows[0].startswith("╭") and rows[-1].startswith("╰") assert sum("Rinse" in row or "Kool" in row or "SWU" in row for row in rows) == 3 # three stations fit: no thumb in the content rows assert not any(row[-2] == "▐" for row in rows[1:-1]) @pytest.mark.asyncio async def test_the_filter_keeps_the_readout_in_step() -> None: app = app_for("somafm") async with app.run_test(size=(100, 26)) as pilot: await pilot.pause() await pilot.press("slash") await pilot.pause() app.query_one("SearchInput").value = "drone" await pilot.pause() pane = app.query_one(StationList) assert pane.border_title.startswith("STATIONS · /drone · ") filtered = len(app.view) assert pane.border_title.endswith(f"/{filtered}")