"""Tests for gui/tray.py module.""" from unittest.mock import MagicMock, patch class TestTrayIconInit: """Tests for TrayIcon initialization.""" def test_tray_icon_stores_app_reference(self): """TrayIcon stores reference to the app.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) assert tray.app is mock_app def test_tray_icon_initially_not_supported(self): """TrayIcon starts with supported=False.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) assert tray.supported is False def test_tray_icon_initially_no_backend(self): """TrayIcon starts with no backend.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) assert tray.backend is None def test_tray_icon_indicator_initially_none(self): """TrayIcon indicator is None initially.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) assert tray.indicator is None class TestTrayIconAppIndicator: """Tests for AppIndicator3 backend.""" def test_try_appindicator_sets_backend(self): """Successful AppIndicator3 sets backend correctly.""" with patch("gui.tray.threading"): # Mock gi and GTK modules mock_gi = MagicMock() mock_gtk = MagicMock() mock_appindicator = MagicMock() mock_glib = MagicMock() mock_gi.require_version = MagicMock() mock_gi.repository.Gtk = mock_gtk mock_gi.repository.AppIndicator3 = mock_appindicator mock_gi.repository.GLib = mock_glib with patch.dict( "sys.modules", {"gi": mock_gi, "gi.repository": MagicMock()} ): from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) # Mock the imports inside _try_appindicator with patch("builtins.__import__", side_effect=ImportError): result = tray._try_appindicator() # Should fail gracefully when imports fail assert result is False def test_try_appindicator_handles_import_error(self): """AppIndicator3 failure is handled gracefully.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) # Without mocking gi, it should fail gracefully result = tray._try_appindicator() # Should return False (not crash) assert result is False class TestTrayIconPystray: """Tests for pystray backend.""" def test_try_pystray_handles_import_error(self): """pystray failure is handled gracefully.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) # Without pystray installed, should fail gracefully with patch.dict("sys.modules", {"pystray": None}): result = tray._try_pystray() # Should return False (not crash) assert result is False def test_try_pystray_with_mock_success(self): """pystray success sets backend correctly.""" with patch("gui.tray.threading"): mock_pystray = MagicMock() mock_pil_image = MagicMock() mock_pil_draw = MagicMock() mock_image_instance = MagicMock() mock_pil_image.new.return_value = mock_image_instance with patch.dict( "sys.modules", { "pystray": mock_pystray, "PIL": MagicMock(), "PIL.Image": mock_pil_image, "PIL.ImageDraw": mock_pil_draw, }, ): from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() TrayIcon(mock_app) # The actual test - mocked pystray should work # (Implementation detail: this tests the exception path) class TestTrayIconCallbacks: """Tests for tray icon callback methods.""" def test_pystray_show_schedules_show_window(self): """_pystray_show schedules _show_window on main thread.""" from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() tray = TrayIcon(mock_app) tray._pystray_show() mock_app.root.after.assert_called_once() # First arg should be 0 (immediate) assert mock_app.root.after.call_args[0][0] == 0 def test_pystray_start_schedules_start(self): """_pystray_start schedules _start on main thread.""" from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() tray = TrayIcon(mock_app) tray._pystray_start() mock_app.root.after.assert_called_once() def test_pystray_stop_schedules_stop(self): """_pystray_stop schedules _stop on main thread.""" from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() tray = TrayIcon(mock_app) tray._pystray_stop() mock_app.root.after.assert_called_once() def test_pystray_test_schedules_api_test(self): """_pystray_test schedules _api_test on main thread.""" from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() tray = TrayIcon(mock_app) tray._pystray_test() mock_app.root.after.assert_called_once() def test_pystray_quit_schedules_close(self): """_pystray_quit schedules _close_completely on main thread.""" from gui.tray import TrayIcon mock_app = MagicMock() mock_app.root = MagicMock() tray = TrayIcon(mock_app) tray._pystray_quit() mock_app.root.after.assert_called_once() class TestTrayIconUpdateStatus: """Tests for status update method.""" def test_update_status_noop_when_not_supported(self): """update_status does nothing when tray not supported.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) tray.supported = False # Should not raise tray.update_status(running=True) tray.update_status(running=False) class TestTrayIconStop: """Tests for stop method.""" def test_stop_handles_no_indicator(self): """stop handles case when indicator is None.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) tray.indicator = None tray.backend = None # Should not raise tray.stop() def test_stop_pystray_calls_indicator_stop(self): """stop calls indicator.stop() for pystray backend.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) tray.backend = "pystray" tray.indicator = MagicMock() tray.stop() tray.indicator.stop.assert_called_once() def test_stop_handles_exception(self): """stop handles exceptions gracefully.""" from gui.tray import TrayIcon mock_app = MagicMock() tray = TrayIcon(mock_app) tray.backend = "pystray" tray.indicator = MagicMock() tray.indicator.stop.side_effect = Exception("test error") # Should not raise tray.stop()