"""Tests for desktop tray runner — pystray integration.""" import sys from unittest.mock import MagicMock, patch import pytest from i2p_apps.desktop.tray import TrayConfig, TrayMenu class TestTrayRunnerImport: """Test that runner handles missing pystray gracefully.""" def test_runner_raises_without_pystray(self): """TrayRunner should raise RuntimeError if pystray is not installed.""" with patch.dict(sys.modules, {"pystray": None, "PIL": None, "PIL.Image": None}): # Force re-import with pystray unavailable import importlib import i2p_apps.desktop.runner as runner_mod # Patch the module-level flag orig = runner_mod._HAS_PYSTRAY runner_mod._HAS_PYSTRAY = False try: with pytest.raises(RuntimeError, match="pystray is required"): runner_mod.TrayRunner() finally: runner_mod._HAS_PYSTRAY = orig class TestTrayRunnerActions: """Test action dispatch (mocked pystray).""" def _make_runner(self): """Create a TrayRunner with pystray mocked.""" from i2p_apps.desktop.runner import TrayRunner # If pystray is not installed, mock the flag import i2p_apps.desktop.runner as mod orig = mod._HAS_PYSTRAY mod._HAS_PYSTRAY = True try: runner = object.__new__(TrayRunner) runner._config = TrayConfig() runner._menu = TrayMenu() runner._icon = MagicMock() return runner finally: mod._HAS_PYSTRAY = orig def test_open_console_action(self): """open_console should call webbrowser.open.""" runner = self._make_runner() with patch("i2p_apps.desktop.runner.webbrowser") as mock_wb: runner._dispatch_action("open_console") mock_wb.open.assert_called_once_with("http://127.0.0.1:7657") def test_quit_action_stops_icon(self): """quit action should stop the tray icon.""" runner = self._make_runner() runner._dispatch_action("quit") runner._icon.stop.assert_called_once() def test_shutdown_signals_router(self): """shutdown_graceful should POST to router API.""" runner = self._make_runner() with patch("i2p_apps.desktop.runner.TrayRunner._signal_router") as mock_sig: runner._dispatch_action("shutdown_graceful") mock_sig.assert_called_once_with("shutdown") def test_restart_signals_router(self): """restart should POST to router API.""" runner = self._make_runner() with patch("i2p_apps.desktop.runner.TrayRunner._signal_router") as mock_sig: runner._dispatch_action("restart") mock_sig.assert_called_once_with("restart") class TestWindowsService: """Test Windows service wrapper can be imported on all platforms.""" def test_import_service_module(self): """The service module should be importable even without pywin32.""" from pathlib import Path import importlib.util # Find service.py relative to this test file's repo root repo_root = Path(__file__).resolve().parents[2] service_path = repo_root / "packaging" / "windows" / "service.py" if not service_path.exists(): pytest.skip("packaging/windows/service.py not found") sys.path.insert(0, str(service_path.parent)) try: spec = importlib.util.spec_from_file_location("service", str(service_path)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) # On non-Windows, _HAS_PYWIN32 should be False assert hasattr(mod, "_HAS_PYWIN32") assert hasattr(mod, "_run_router") assert hasattr(mod, "cli") finally: sys.path.pop(0) class TestIconImage: """Test icon image generation.""" def test_create_icon_without_pystray(self): """Should return None if pystray is not available.""" from i2p_apps.desktop.runner import _create_icon_image import i2p_apps.desktop.runner as mod orig = mod._HAS_PYSTRAY mod._HAS_PYSTRAY = False try: assert _create_icon_image() is None finally: mod._HAS_PYSTRAY = orig class TestMainEntryPoint: """Test the main() entry point.""" def test_main_exits_without_pystray(self, capsys): """main() should print error and exit if pystray missing.""" import i2p_apps.desktop.runner as mod orig = mod._HAS_PYSTRAY mod._HAS_PYSTRAY = False try: with pytest.raises(SystemExit) as exc_info: mod.main() assert exc_info.value.code == 1 finally: mod._HAS_PYSTRAY = orig