"""Integration tests for GUI widget methods (requires real Qt runtime). These tests create actual QWidget instances and test the new/modified methods that were added during the recent code quality refactoring. """ from unittest import mock import pytest pytest.importorskip("PySide6.QtWidgets") from PySide6.QtCore import QTimer from PySide6.QtWidgets import QApplication, QPushButton # ═══════════════════════════════════════════════════════════════════════════════ # Fixtures # ═══════════════════════════════════════════════════════════════════════════════ @pytest.fixture(scope="module") def app(): """Create (or reuse) the singleton QApplication.""" inst = QApplication.instance() if inst is None: inst = QApplication([]) yield inst @pytest.fixture def player_mock(): return mock.MagicMock(name="player") @pytest.fixture def visualizer_mock(): return mock.MagicMock(name="visualizer") # ═══════════════════════════════════════════════════════════════════════════════ # AudioVisualizer # ═══════════════════════════════════════════════════════════════════════════════ class TestAudioVisualizer: """Tests for AudioVisualizer new methods.""" def test_reset_activity(self, app): """reset_activity sets _last_activity to 0.""" from gui import AudioVisualizer viz = AudioVisualizer() viz._last_activity = 999 viz.reset_activity() assert viz._last_activity == 0 def test_show_and_hide(self, app): """Visualizer can be shown and hidden without error.""" from gui import AudioVisualizer viz = AudioVisualizer() viz.show() viz.hide() assert not viz.isVisible() # ═══════════════════════════════════════════════════════════════════════════════ # PlaybackBar # ═══════════════════════════════════════════════════════════════════════════════ class TestPlaybackBar: """Tests for PlaybackBar new/modified methods.""" def test_clear_seek_flag(self, app, player_mock, visualizer_mock): """_clear_seek_flag sets _updating_progress to False.""" from gui import PlaybackBar bar = PlaybackBar(player=player_mock, visualizer=visualizer_mock) bar._updating_progress = True bar._clear_seek_flag() assert bar._updating_progress is False def test_flush_volume_stops_timer_and_saves(self, app, player_mock, visualizer_mock): """flush_volume persists the volume and stops the debounce timer.""" from gui import PlaybackBar bar = PlaybackBar(player=player_mock, visualizer=visualizer_mock) bar._saved_volume = 42 # Create and start a timer so we can verify it gets stopped bar._vol_save_timer = QTimer() bar._vol_save_timer.setSingleShot(True) bar._vol_save_timer.start(5000) # long interval so it stays active assert bar._vol_save_timer.isActive() with mock.patch("gui.save_volume") as mock_save: bar.flush_volume() mock_save.assert_called_once_with(42) assert not bar._vol_save_timer.isActive() def test_flush_volume_no_timer(self, app, player_mock, visualizer_mock): """flush_volume does not crash when _vol_save_timer doesn't exist.""" from gui import PlaybackBar bar = PlaybackBar(player=player_mock, visualizer=visualizer_mock) # In PlaybackBar.__init__, _vol_save_timer is created lazily. # Delete it to test the graceful fallback. if hasattr(bar, "_vol_save_timer"): del bar._vol_save_timer bar._saved_volume = 75 with mock.patch("gui.save_volume") as mock_save: bar.flush_volume() mock_save.assert_called_once_with(75) def test_show_error_uses_method_ref(self, app, player_mock, visualizer_mock): """show_error schedules _clear_error via method ref, not lambda.""" from gui import PlaybackBar bar = PlaybackBar(player=player_mock, visualizer=visualizer_mock) with mock.patch.object(bar, "_clear_error") as mock_clear: bar.show_error("Test error") # The error text should be set assert "Test error" in bar._np_title.text() # _clear_error should be scheduled (not called immediately) mock_clear.assert_not_called() # ═══════════════════════════════════════════════════════════════════════════════ # SearchPage # ═══════════════════════════════════════════════════════════════════════════════ class TestSearchPage: """Tests for SearchPage new methods.""" def test_on_show_more_clicked_from_button(self, app): """_on_show_more_clicked expands the correct category via button click.""" from gui import SearchPage page = SearchPage() page._videos_expanded = False # Create a button and set it up like _add_show_more does btn = QPushButton("⋯ Show more") btn.setProperty("category", "videos") btn.clicked.connect(page._on_show_more_clicked) with mock.patch.object(page, "_toggle_expand") as mock_toggle: btn.click() mock_toggle.assert_called_once_with("videos") def test_on_show_more_clicked_songs(self, app): """_on_show_more_clicked expands songs category.""" from gui import SearchPage page = SearchPage() page._songs_expanded = False btn = QPushButton("⋯ Show more") btn.setProperty("category", "songs") btn.clicked.connect(page._on_show_more_clicked) with mock.patch.object(page, "_toggle_expand") as mock_toggle: btn.click() mock_toggle.assert_called_once_with("songs") def test_toggle_expand_videos(self, app): """_toggle_expand sets videos expanded flag.""" from gui import SearchPage page = SearchPage() page._videos_expanded = False with mock.patch.object(page, "_rebuild"): page._toggle_expand("videos") assert page._videos_expanded is True def test_toggle_expand_songs(self, app): """_toggle_expand sets songs expanded flag.""" from gui import SearchPage page = SearchPage() page._songs_expanded = False with mock.patch.object(page, "_rebuild"): page._toggle_expand("songs") assert page._songs_expanded is True # ═══════════════════════════════════════════════════════════════════════════════ # TunettiWindow accessors # ═══════════════════════════════════════════════════════════════════════════════ class TestTunettiWindowAccessors: """Tests for the public accessor methods on TunettiWindow.""" @pytest.fixture def window(self, app): """Create and properly clean up a TunettiWindow.""" from gui import TunettiWindow win = TunettiWindow() yield win win.player.shutdown() win.rpc.stop() win.db.close() win.close() def test_get_visualizer(self, window): """get_visualizer returns the AudioVisualizer instance.""" from gui import AudioVisualizer assert isinstance(window.get_visualizer(), AudioVisualizer) def test_get_rpc(self, window): """get_rpc returns the DiscordRPC instance.""" from discord_rpc import DiscordRPC assert isinstance(window.get_rpc(), DiscordRPC) def test_get_player(self, window): """get_player returns the AudioPlayer instance.""" from player import AudioPlayer assert isinstance(window.get_player(), AudioPlayer)