245 lines
8.3 KiB
Python
245 lines
8.3 KiB
Python
"""The colour palettes: sixteen ladders of ANSI names, and the `c` key."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from beefy_player import config
|
|
from beefy_player.app import BeefyPlayerApp
|
|
from beefy_player.palettes import (
|
|
DEFAULT_PALETTE,
|
|
PALETTE_ORDER,
|
|
PALETTES,
|
|
palette_stops,
|
|
)
|
|
from beefy_player.widgets import frame
|
|
from beefy_player.widgets.help import HelpScreen
|
|
from beefy_player.widgets.station_list import StationList
|
|
|
|
ANSI_NAMES = {
|
|
"black",
|
|
"red",
|
|
"green",
|
|
"yellow",
|
|
"blue",
|
|
"magenta",
|
|
"cyan",
|
|
"white",
|
|
"bright_black",
|
|
"bright_red",
|
|
"bright_green",
|
|
"bright_yellow",
|
|
"bright_blue",
|
|
"bright_magenta",
|
|
"bright_cyan",
|
|
"bright_white",
|
|
}
|
|
|
|
|
|
def test_there_are_sixteen_palettes_of_ansi_names() -> None:
|
|
assert len(PALETTES) == 16
|
|
assert len(PALETTE_ORDER) == 16
|
|
assert len(set(PALETTE_ORDER)) == 16
|
|
assert DEFAULT_PALETTE in PALETTES
|
|
|
|
for name, stops in PALETTES.items():
|
|
assert len(stops) >= 3, name
|
|
assert set(stops) <= ANSI_NAMES, (name, set(stops) - ANSI_NAMES)
|
|
# a ladder, not a list: the quiet end must differ from the hot end
|
|
assert stops[0] != stops[-1], name
|
|
|
|
|
|
def test_a_broken_palette_name_falls_back_to_the_default() -> None:
|
|
assert palette_stops("mono") == PALETTES["mono"]
|
|
assert palette_stops(" MONO ") == PALETTES["mono"]
|
|
assert palette_stops("no-such-palette") == PALETTES[DEFAULT_PALETTE]
|
|
assert palette_stops(None) == PALETTES[DEFAULT_PALETTE]
|
|
|
|
|
|
def test_the_config_remembers_the_palette() -> None:
|
|
assert config.Config.load().palette == DEFAULT_PALETTE
|
|
config.Config(palette="ember").save()
|
|
assert config.Config.load().palette == "ember"
|
|
|
|
|
|
def test_an_unknown_palette_in_the_config_is_ignored() -> None:
|
|
config.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
config.CONFIG_PATH.write_text('{"palette": "chartreuse"}')
|
|
assert config.Config.load().palette == DEFAULT_PALETTE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_c_key_cycles_and_remembers_the_palette() -> None:
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(100, 30)) as pilot:
|
|
await pilot.pause()
|
|
start = app.palette
|
|
assert start == DEFAULT_PALETTE
|
|
|
|
await pilot.press("c")
|
|
await pilot.pause()
|
|
expected = PALETTE_ORDER[(PALETTE_ORDER.index(start) + 1) % len(PALETTE_ORDER)]
|
|
assert app.palette == expected
|
|
assert app.config.palette == expected
|
|
assert "palette" in app.notice
|
|
|
|
# the choice survives a restart
|
|
config.Config(palette=app.palette).save()
|
|
assert config.Config.load().palette == expected
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_pane_corners_follow_the_palette() -> None:
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(100, 30)) as pilot:
|
|
await pilot.pause()
|
|
pane = app.query_one(StationList)
|
|
|
|
app.palette = "mono"
|
|
mono = [str(span.style) for span in pane.render().renderables[0].spans]
|
|
app.palette = "ember"
|
|
ember = [str(span.style) for span in pane.render().renderables[0].spans]
|
|
|
|
assert mono != ember
|
|
assert any("bright_white" in style for style in mono)
|
|
# ember's hot end is white too, but its quiet end is not
|
|
assert any(PALETTES["ember"][-2] in style for style in ember)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_spectrum_rows_use_the_active_palette() -> None:
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(100, 30)) as pilot:
|
|
await pilot.pause()
|
|
spectrum = app.query_one("SpectrumPanel")
|
|
|
|
def colours(styles: list[str]) -> set[str]:
|
|
return {
|
|
style.replace("dim ", "").replace("bold ", "").strip()
|
|
for style in styles
|
|
}
|
|
|
|
app.palette = "mono"
|
|
spectrum.refresh()
|
|
await pilot.pause()
|
|
assert colours(spectrum._row_palette(12, 0, False)) <= set(PALETTES["mono"])
|
|
|
|
app.palette = "neon"
|
|
spectrum.refresh()
|
|
await pilot.pause()
|
|
assert colours(spectrum._row_palette(12, 0, False)) <= set(PALETTES["neon"])
|
|
|
|
|
|
def test_the_corner_fade_walks_back_down_the_ladder() -> None:
|
|
stops = PALETTES["spectrum"]
|
|
assert frame.corner_fade(0, stops) == stops[-1]
|
|
assert frame.corner_fade(1, stops) == stops[-2]
|
|
assert frame.corner_fade(3, stops) == stops[-4]
|
|
# the fade is short: past four cells it is just the border grey
|
|
assert frame.corner_fade(4, stops) == frame.EDGE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_help_scrolls_when_the_terminal_is_short() -> None:
|
|
"""The help page is ~56 rows: it must be readable on a 24-row screen."""
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(80, 24)) as pilot:
|
|
await pilot.pause()
|
|
await pilot.press("question_mark")
|
|
await pilot.pause()
|
|
body = app.screen.query_one("#help")
|
|
assert body.size.height < body.virtual_size.height
|
|
assert body.max_scroll_y > 0
|
|
assert body.scroll_y == 0
|
|
|
|
await pilot.press("pagedown")
|
|
await pilot.pause()
|
|
assert body.scroll_y > 0
|
|
|
|
await pilot.press("end")
|
|
await pilot.pause()
|
|
assert body.scroll_y == body.max_scroll_y
|
|
|
|
await pilot.press("home")
|
|
await pilot.pause()
|
|
assert body.scroll_y == 0
|
|
|
|
await pilot.press("x")
|
|
await pilot.pause()
|
|
assert not isinstance(app.screen, HelpScreen)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_tall_terminal_needs_no_scrolling() -> None:
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(120, 70)) as pilot:
|
|
await pilot.pause()
|
|
await pilot.press("question_mark")
|
|
await pilot.pause()
|
|
body = app.screen.query_one("#help")
|
|
assert body.max_scroll_y == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_help_lists_every_palette_and_marks_the_active_one() -> None:
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(120, 64)) as pilot:
|
|
await pilot.pause()
|
|
app.palette = "ocean"
|
|
await pilot.press("question_mark")
|
|
await pilot.pause()
|
|
text = html.unescape(app.export_screenshot()).replace("\xa0", " ")
|
|
text = re.sub(r"<[^>]+>", "", text)
|
|
|
|
assert "PALETTES" in text
|
|
assert "c cycles them" in text
|
|
for name in PALETTE_ORDER:
|
|
assert name in text, name
|
|
assert "● ocean" in text
|
|
# each palette shows a swatch of eight blocks
|
|
assert "████████" in text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_help_footer_attributes_the_build() -> None:
|
|
"""One line: the version and the project URL — no credits essay."""
|
|
from beefy_player import __version__
|
|
|
|
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(110, 70)) as pilot:
|
|
await pilot.pause()
|
|
await pilot.press("question_mark")
|
|
await pilot.pause()
|
|
body = app.screen.query_one("#help")
|
|
body.scroll_end(animate=False)
|
|
await pilot.pause()
|
|
|
|
text = html.unescape(app.export_screenshot()).replace("\xa0", " ")
|
|
text = re.sub(r"<[^>]+>", "", text)
|
|
|
|
assert f"beefy player {__version__}" in text
|
|
assert "https://tildeforge.dev/beefcarver/beefy-player" in text
|
|
# the old explanatory blurb is gone
|
|
assert "playerctl" not in text
|
|
assert "parec" not in text
|
|
assert "Stations and song history come from" not in text
|
|
|
|
|
|
@pytest.mark.parametrize("slug", ["bassdrive", "somafm"])
|
|
@pytest.mark.asyncio
|
|
async def test_the_help_has_no_pane_explainer(slug: str) -> None:
|
|
"""No catalogue gets a paragraph about its left pane."""
|
|
app = BeefyPlayerApp(provider=slug, autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(110, 60)) as pilot:
|
|
await pilot.pause()
|
|
await pilot.press("question_mark")
|
|
await pilot.pause()
|
|
text = html.unescape(app.export_screenshot()).replace("\xa0", " ")
|
|
text = re.sub(r"<[^>]+>", "", text)
|
|
assert "is a single stream" not in text
|
|
assert "multi-station catalogues" not in text
|
|
assert "CATALOGUES" in text
|