feat(ui): add catalogue picker, schedule pane and per-provider hero
This commit is contained in:
1 parent
058d336a2c
commit
b31ad3f6fb
16 files changed
+1211
-65
No files matched your search
+106
-1
@@ -11,6 +11,8 @@ from beefy_player.models import Song, Station
|
||||
from beefy_player.providers import PROVIDERS, available_providers, get_provider
|
||||
from beefy_player.providers.base import RadioProvider
|
||||
from beefy_player.providers.somafm import SomaFMProvider
|
||||
from beefy_player.providers.tilderadio import TilderadioProvider
|
||||
from beefy_player.widgets.header import HeaderBar
|
||||
|
||||
|
||||
def rendered_text(app) -> str:
|
||||
@@ -19,9 +21,10 @@ def rendered_text(app) -> str:
|
||||
|
||||
|
||||
def test_registry_lists_somafm_as_the_default() -> None:
|
||||
assert available_providers() == ("somafm",)
|
||||
assert available_providers() == ("somafm", "tilderadio")
|
||||
assert isinstance(get_provider(), SomaFMProvider)
|
||||
assert get_provider("somafm") is get_provider()
|
||||
assert isinstance(get_provider("tilderadio"), TilderadioProvider)
|
||||
# a stale slug in the config file must not stop the app from starting
|
||||
assert get_provider("no-such-catalogue") is get_provider()
|
||||
|
||||
@@ -128,3 +131,105 @@ def test_environment_variables_prefer_the_beefy_prefix(monkeypatch) -> None:
|
||||
|
||||
monkeypatch.delenv("SOMAFM_IMAGES")
|
||||
assert config.env_var("IMAGES") == ""
|
||||
|
||||
|
||||
def test_tilderadio_replaces_the_station_list_with_a_schedule() -> None:
|
||||
provider = TilderadioProvider()
|
||||
assert provider.has_schedule is True
|
||||
# the SomaFM wordmark's slot now holds tilderadio's own art and motto
|
||||
assert provider.hero and not provider.hero_gradient
|
||||
assert provider.hero_lines == (
|
||||
"TILDERADIO",
|
||||
"STREAMED BY / FOR USERS OF THE",
|
||||
"TILDEVERSE",
|
||||
)
|
||||
assert SomaFMProvider().has_schedule is False
|
||||
|
||||
|
||||
class _CatalogueApp:
|
||||
"""A headless app on a fixed catalogue, for the picker tests."""
|
||||
|
||||
def __init__(self, provider: str = "somafm") -> None:
|
||||
from beefy_player.app import BeefyPlayerApp
|
||||
|
||||
self.app = BeefyPlayerApp(
|
||||
provider=provider, autoplay=False, offline=True, audio=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p_opens_the_picker_and_switches_catalogue() -> None:
|
||||
from beefy_player.widgets.providers import ProviderScreen
|
||||
from beefy_player.widgets.schedule import SchedulePanel
|
||||
from beefy_player.widgets.station_list import StationList
|
||||
|
||||
app = _CatalogueApp().app
|
||||
async with app.run_test(size=(110, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
assert app.provider.id == "somafm"
|
||||
assert app.query_one(StationList).display is True
|
||||
assert app.query_one(SchedulePanel).display is False
|
||||
assert app.query_one(HeaderBar).styles.height.value == 4
|
||||
|
||||
await pilot.press("p")
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, ProviderScreen)
|
||||
|
||||
await pilot.press("2") # jump straight to tilderadio
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
|
||||
assert app.provider.id == "tilderadio"
|
||||
assert app.config.provider == "tilderadio"
|
||||
assert [station.id for station in app.stations] == ["tilderadio"]
|
||||
# the left pane becomes the schedule, and the hero grows
|
||||
assert app.query_one(StationList).display is False
|
||||
assert app.query_one(SchedulePanel).display is True
|
||||
assert app.query_one(HeaderBar).styles.height.value == 8
|
||||
assert "tilderadio" in app.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_escape_closes_the_picker_without_switching() -> None:
|
||||
app = _CatalogueApp().app
|
||||
async with app.run_test(size=(110, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("p")
|
||||
await pilot.pause()
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert app.provider.id == "somafm"
|
||||
assert app.config.provider != "tilderadio"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_names_both_catalogues_and_the_switch_key() -> None:
|
||||
app = _CatalogueApp().app
|
||||
async with app.run_test(size=(120, 60)) as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("question_mark")
|
||||
await pilot.pause()
|
||||
text = rendered_text(app)
|
||||
assert "CATALOGUES" in text
|
||||
assert "somafm" in text and "tilderadio" in text
|
||||
assert "switch station catalogue" in text
|
||||
|
||||
|
||||
def test_config_remembers_a_station_per_catalogue() -> None:
|
||||
config.Config(last_stations={"somafm": "defcon", "tilderadio": "tilderadio"}).save()
|
||||
loaded = config.Config.load()
|
||||
assert loaded.last_station_for("somafm") == "defcon"
|
||||
assert loaded.last_station_for("tilderadio") == "tilderadio"
|
||||
# a catalogue nobody has tuned yet falls back to the legacy single field
|
||||
assert loaded.last_station_for("no-such-catalogue") == loaded.last_station
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_terminals_crop_the_hero_but_keep_the_ticker() -> None:
|
||||
app = _CatalogueApp("tilderadio").app
|
||||
async with app.run_test(size=(90, 24)) as pilot:
|
||||
await pilot.pause()
|
||||
header = app.query_one(HeaderBar)
|
||||
assert header.styles.height.value <= 6
|
||||
lines = [line.plain for line in header.render().renderables]
|
||||
assert any("STOPPED" in line or "PLAYING" in line for line in lines)
|
||||
Reference in new issue
Block a user