✅ | Add widget integration tests for SonarQube coverage
Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 1m22s
Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 1m22s
This commit is contained in:
226
tests/test_gui_widgets.py
Normal file
226
tests/test_gui_widgets.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user