"""Round 11: per-area grayscale color preferences. Five sliders (black->white) tune: the outer chrome background, the rounded button boxes, the now-playing panel, the now-playing text, and the row stripes. Defaults are None ("inherit current") so nothing changes until the user drags. """ from unittest.mock import MagicMock, patch from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtGui import QPalette, QColor from lintunes import theme 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 from lintunes.gui.preferences_dialog import PreferencesDialog, _COLOR_ROWS def _mock_player(): 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)) # -------------------------------------------------------------------------- # theme grayscale helpers # -------------------------------------------------------------------------- class TestThemeGrays: def test_gray_builds_opaque_gray_clamped(self): assert theme.gray(128) == QColor(128, 128, 128) assert theme.gray(-5) == QColor(0, 0, 0) assert theme.gray(999) == QColor(255, 255, 255) def test_fixed_defaults(self): assert theme.default_gray("color_now_playing_bg") == 255 assert theme.default_gray("color_now_playing_text") == 0 def test_gray_value_uses_default_when_unset(self, qapp, tmp_path): prefs = Preferences(tmp_path) assert prefs.get("color_now_playing_text") is None assert theme.gray_value(prefs, "color_now_playing_text") == 0 prefs.set("color_now_playing_text", 200) assert theme.gray_value(prefs, "color_now_playing_text") == 200 # -------------------------------------------------------------------------- # apply_theme palette overrides (background -> Window, stripe -> AlternateBase) # -------------------------------------------------------------------------- class TestApplyThemeOverrides: def test_background_and_stripe_recolor_palette(self, qapp, tmp_path): prefs = Preferences(tmp_path) prefs.set("color_background", 100) prefs.set("color_stripe", 40) saved = QPalette(qapp.palette()) try: theme.apply_theme(qapp, prefs) pal = qapp.palette() assert pal.color(QPalette.ColorGroup.Active, QPalette.ColorRole.Window) == theme.gray(100) assert pal.color(QPalette.ColorGroup.Active, QPalette.ColorRole.AlternateBase) == theme.gray(40) finally: qapp.setPalette(saved) def test_unset_leaves_base_window(self, qapp, tmp_path): prefs = Preferences(tmp_path) saved = QPalette(qapp.palette()) try: theme.apply_theme(qapp, prefs) # no overrides set base_window = theme._base().color( QPalette.ColorGroup.Active, QPalette.ColorRole.Window) assert qapp.palette().color(QPalette.ColorGroup.Active, QPalette.ColorRole.Window) == base_window finally: qapp.setPalette(saved) # -------------------------------------------------------------------------- # TransportBar applies the stylesheet-side colors (button boxes, panel, text) # -------------------------------------------------------------------------- class TestTransportColors: def _build(self, tmp_path, prefs): manager = LibraryManager( Library(tracks={1: Track(track_id=1, name="One")}), tmp_path) return TransportBar(_mock_player(), manager, prefs) def test_colors_reflected_at_build(self, qapp, tmp_path): prefs = Preferences(tmp_path) prefs.set("color_button", 50) prefs.set("color_now_playing_bg", 100) prefs.set("color_now_playing_text", 200) transport = self._build(tmp_path, prefs) assert transport._boxes # transport, shuffle, bpm for box in transport._boxes: assert theme.gray(50).name() in box.styleSheet() assert theme.gray(100).name() in transport._now_playing.styleSheet() assert theme.gray(200).name() in transport._title_label.styleSheet() def test_refresh_theme_repaints_after_change(self, qapp, tmp_path): prefs = Preferences(tmp_path) transport = self._build(tmp_path, prefs) assert "#000000" in transport._title_label.styleSheet() # default black prefs.set_live("color_now_playing_text", 255) transport.refresh_theme() assert "#ffffff" in transport._title_label.styleSheet() # -------------------------------------------------------------------------- # Preferences.set_live: notify without persisting # -------------------------------------------------------------------------- class TestSetLive: def test_updates_memory_and_emits_without_writing(self, tmp_path): prefs = Preferences(tmp_path) seen = [] prefs.changed.connect(lambda: seen.append(True)) prefs.set_live("color_background", 123) assert prefs.get("color_background") == 123 assert seen == [True] assert not (tmp_path / "preferences.json").exists() prefs.save() assert Preferences(tmp_path).get("color_background") == 123 # -------------------------------------------------------------------------- # PreferencesDialog colors group # -------------------------------------------------------------------------- class _FakeLastFm(QObject): login_finished = pyqtSignal(bool, str) status_message = pyqtSignal(str) def is_logged_in(self): return False class TestPreferencesDialogColors: def test_sliders_built_and_drive_set_live(self, qapp, tmp_path): prefs = Preferences(tmp_path) dialog = PreferencesDialog(prefs, _FakeLastFm()) keys = [k for k, _ in _COLOR_ROWS] assert set(dialog._color_sliders) == set(keys) for key in keys: assert dialog._color_sliders[key].value() == theme.gray_value(prefs, key) slider = dialog._color_sliders["color_stripe"] target = 0 if slider.value() != 0 else 255 slider.setValue(target) assert prefs.get("color_stripe") == target assert dialog._color_save_timer.isActive()