"""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