Add volume slider to the transport bar
A slim horizontal master-volume slider in its own slot between the visualizer and the timeline (TASKS.md). Perceptual->linear gain so it ramps like iTunes; persists to preferences.json (debounced so it doesn't thrash the theme). Clicking jumps to the spot via a new ClickJumpSlider base extracted from SeekSlider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
104
tests/test_round10.py
Normal file
104
tests/test_round10.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Round 10: master output volume slider in the transport bar.
|
||||
|
||||
1. Player.set_volume / volume round-trip (logical 0..1, clamped).
|
||||
2. TransportBar builds a volume slider initialized from the prefs value and
|
||||
sets the player volume to match.
|
||||
3. Moving the slider updates the player live and (after the debounce) persists
|
||||
the new volume to preferences.json.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lintunes.models import Library, Track
|
||||
from lintunes.library_manager import LibraryManager
|
||||
from lintunes.preferences import Preferences
|
||||
from lintunes import player as player_module
|
||||
from lintunes.player import Player
|
||||
from lintunes.gui.transport import TransportBar, ClickJumpSlider
|
||||
|
||||
|
||||
def _mock_player():
|
||||
"""A real Player with the Qt multimedia backend stubbed out (it blocks on
|
||||
init headless — see tests/test_player.py). The audio output becomes a mock,
|
||||
so set_volume's setVolume call is a harmless no-op."""
|
||||
library = Library(tracks={1: Track(track_id=1, name="One")})
|
||||
with patch.multiple(
|
||||
player_module,
|
||||
QMediaPlayer=MagicMock(),
|
||||
QAudioOutput=MagicMock(),
|
||||
QAudioBufferOutput=MagicMock(),
|
||||
QMediaDevices=MagicMock(),
|
||||
):
|
||||
return Player(MagicMock(library=library))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 1. Player volume API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestPlayerVolume:
|
||||
def test_round_trip(self, qapp):
|
||||
player = _mock_player()
|
||||
player.set_volume(0.5)
|
||||
assert player.volume() == 0.5
|
||||
# The linear gain handed to QAudioOutput is set (perceptual->linear).
|
||||
player._audio.setVolume.assert_called()
|
||||
|
||||
def test_clamps_out_of_range(self, qapp):
|
||||
player = _mock_player()
|
||||
player.set_volume(1.5)
|
||||
assert player.volume() == 1.0
|
||||
player.set_volume(-0.3)
|
||||
assert player.volume() == 0.0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 2 + 3. TransportBar volume slider
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestTransportVolumeSlider:
|
||||
def _build(self, tmp_path, prefs):
|
||||
player = _mock_player()
|
||||
manager = LibraryManager(
|
||||
Library(tracks={1: Track(track_id=1, name="One")}), tmp_path)
|
||||
return TransportBar(player, manager, prefs), player
|
||||
|
||||
def test_slider_initialized_from_prefs(self, qapp, tmp_path):
|
||||
prefs = Preferences(tmp_path)
|
||||
prefs.set("volume", 0.3)
|
||||
transport, player = self._build(tmp_path, prefs)
|
||||
|
||||
assert transport._volume_slider.value() == 30
|
||||
assert player.volume() == 0.3
|
||||
|
||||
def test_moving_slider_updates_player_live(self, qapp, tmp_path):
|
||||
prefs = Preferences(tmp_path)
|
||||
transport, player = self._build(tmp_path, prefs)
|
||||
|
||||
transport._volume_slider.setValue(20)
|
||||
|
||||
assert player.volume() == 0.2
|
||||
|
||||
def test_save_persists_to_disk(self, qapp, tmp_path):
|
||||
prefs = Preferences(tmp_path)
|
||||
transport, player = self._build(tmp_path, prefs)
|
||||
|
||||
transport._volume_slider.setValue(20)
|
||||
# The debounce timer normally fires this; call it directly.
|
||||
transport._save_volume()
|
||||
|
||||
# A freshly loaded Preferences sees the persisted value.
|
||||
assert Preferences(tmp_path).get("volume") == 0.2
|
||||
|
||||
def test_click_jumps_to_position(self, qapp, tmp_path):
|
||||
"""Click-to-jump: the slider lands on the clicked spot, not a step."""
|
||||
prefs = Preferences(tmp_path)
|
||||
transport, player = self._build(tmp_path, prefs)
|
||||
slider = transport._volume_slider
|
||||
|
||||
assert isinstance(slider, ClickJumpSlider)
|
||||
slider.resize(100, 20)
|
||||
# A click at the far-left maps to the minimum, mid to ~middle.
|
||||
assert slider._value_at(0) == 0
|
||||
assert 40 <= slider._value_at(50) <= 60
|
||||
assert slider._value_at(100) == 100
|
||||
Reference in New Issue
Block a user