diff --git a/tests/test_app.py b/tests/test_app.py index af0df46..227a3d3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone import pytest +from somafm_tui import config from somafm_tui.api import Song from somafm_tui.app import SomaFMApp from somafm_tui.widgets.help import HelpScreen @@ -70,11 +71,11 @@ async def test_boot_navigation_filter_favourite_visualizer_help() -> None: assert app.index == 0 modes = set() - for _ in range(3): + for _ in range(len(config.VISUALIZERS)): await pilot.press("v") await pilot.pause() modes.add(app.visualizer) - assert modes == {"both", "bars", "wave"} + assert modes == set(config.VISUALIZERS) await pilot.press("question_mark") await pilot.pause() diff --git a/tests/test_spectrum_panel.py b/tests/test_spectrum_panel.py new file mode 100644 index 0000000..46954af --- /dev/null +++ b/tests/test_spectrum_panel.py @@ -0,0 +1,103 @@ +"""Rendering tests for the waterfall, radial and braille visualisers.""" + +from __future__ import annotations + +import math + +from somafm_tui import config +from somafm_tui.spectrum import Frame, SpectrumEngine +from somafm_tui.widgets.spectrum import ( + VISUALIZER_LABELS, + WATERFALL_HISTORY, + SpectrumPanel, +) + + +def make_frame(level: float = 0.7) -> Frame: + bands = tuple(0.12 + 0.8 * max(0.0, math.sin(index / 4.0)) for index in range(72)) + peaks = tuple(min(1.0, value + 0.05) for value in bands) + wave = tuple(level * math.sin(point / 9.0) for point in range(192)) + return Frame(bands=bands, peaks=peaks, wave=wave, level=level, status="monitor") + + +def make_panel() -> SpectrumPanel: + return SpectrumPanel(SpectrumEngine()) + + +def test_every_visualizer_has_a_border_label() -> None: + assert set(VISUALIZER_LABELS) == set(config.VISUALIZERS) + + +def test_waterfall_records_one_column_per_tick() -> None: + panel = make_panel() + frame = make_frame() + panel._record(frame, tick=1) + panel._record(frame, tick=2) + panel._record(frame, tick=2) # a second render in the same tick + assert len(panel._history) == 2 + for tick in range(3, WATERFALL_HISTORY + 20): + panel._record(frame, tick) + assert len(panel._history) == WATERFALL_HISTORY + + +def test_waterfall_scrolls_spectrogram_with_bass_at_the_bottom() -> None: + panel = make_panel() + # Quieter highs, louder bass, so the bottom rows must be fuller. + rising = Frame( + bands=tuple((71 - index) / 71 for index in range(72)), + peaks=tuple((71 - index) / 71 for index in range(72)), + wave=(0.0,) * 192, + level=0.5, + status="monitor", + ) + for tick in range(30): + panel._record(rising, tick) + lines = panel._waterfall(40, 12, dim=False) + assert len(lines) == 12 + assert {len(line.plain) for line in lines} == {40} + chars = "".join(line.plain for line in lines) + assert any(char in chars for char in "░▒▓█") + # History that does not fill the panel is padded blank on the left. + assert lines[0].plain[:9].strip() == "" + filled = [sum(1 for char in line.plain if char != " ") for line in lines] + assert filled[-1] > filled[0] + + +def test_radial_spectrum_draws_a_ring_inside_the_panel() -> None: + panel = make_panel() + lines = panel._radial(make_frame(), 61, 21, phase=0, dim=False) + assert len(lines) == 21 + assert {len(line.plain) for line in lines} == {61} + text = "".join(line.plain for line in lines) + assert text.count("█") > 10 + assert text.count("·") > 4 + # The corners are outside the circle. + assert lines[2].plain[:6].strip() == "" + assert lines[2].plain[-6:].strip() == "" + + +def test_radial_falls_back_to_bars_when_too_small() -> None: + panel = make_panel() + lines = panel._radial(make_frame(), 12, 4, phase=0, dim=False) + assert len(lines) == 4 + assert {len(line.plain) for line in lines} == {12} + assert any(char in "".join(line.plain for line in lines) for char in "█▄▅") + + +def test_braille_oscilloscope_uses_subcell_dots() -> None: + panel = make_panel() + lines = panel._braille(make_frame(0.9), 40, 8, phase=0, dim=False) + assert len(lines) == 8 + assert {len(line.plain) for line in lines} == {40} + chars = "".join(line.plain for line in lines) + assert all(char == " " or 0x2800 <= ord(char) <= 0x28FF for char in chars) + dots = [char for char in chars if 0x2800 <= ord(char) <= 0x28FF] + assert len(dots) > 20 + + +def test_braille_keeps_a_baseline_when_silent() -> None: + panel = make_panel() + silent = Frame((0.0,) * 72, (0.0,) * 72, (0.0,) * 192, 0.0, "idle") + lines = panel._braille(silent, 20, 6, phase=0, dim=True) + chars = "".join(line.plain for line in lines) + assert any(0x2800 <= ord(char) <= 0x28FF for char in chars)