import sys import pytest @pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows") class TestCreateIcon: def test_returns_image_for_each_state(self): from PIL import Image from whisper_local.tray._tray import AppState from whisper_local.tray._icon import create_icon for state in AppState: img = create_icon(state) assert isinstance(img, Image.Image) assert img.size == (64, 64) assert img.mode == "RGBA" def test_different_states_have_different_colors(self): from whisper_local.tray._tray import AppState from whisper_local.tray._icon import create_icon waiting = create_icon(AppState.WAITING) recording = create_icon(AppState.RECORDING) assert waiting.tobytes() != recording.tobytes() @pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows") class TestPystrayApp: def test_set_state_updates_icon(self): from unittest.mock import MagicMock, patch from whisper_local.tray._tray import AppState, PystrayApp app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock()) mock_icon = MagicMock() app._icon = mock_icon app.set_state(AppState.RECORDING) assert mock_icon.icon is not None def test_set_state_before_start_is_safe(self): from unittest.mock import MagicMock from whisper_local.tray._tray import AppState, PystrayApp app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock()) app.set_state(AppState.WAITING) # kein Fehler, _icon ist None class TestNoOpTray: def test_start_does_nothing(self): from whisper_local.tray._tray import AppState, NoOpTray tray = NoOpTray() tray.start() # kein Fehler def test_set_state_does_nothing(self): from whisper_local.tray._tray import AppState, NoOpTray tray = NoOpTray() tray.set_state(AppState.RECORDING) # kein Fehler @pytest.mark.skipif(sys.platform != "win32", reason="Theme nur auf Windows") class TestApplySystemTheme: def test_applies_light_theme(self): import tkinter as tk from unittest.mock import patch, MagicMock from whisper_local.tray._theme import apply_system_theme root = tk.Tk() root.withdraw() try: with patch("darkdetect.theme", return_value="Light"), \ patch("sv_ttk.set_theme") as mock_set: apply_system_theme(root) mock_set.assert_called_once_with("light") finally: root.destroy() def test_applies_dark_theme(self): import tkinter as tk from unittest.mock import patch from whisper_local.tray._theme import apply_system_theme root = tk.Tk() root.withdraw() try: with patch("darkdetect.theme", return_value="Dark"), \ patch("sv_ttk.set_theme") as mock_set: apply_system_theme(root) mock_set.assert_called_once_with("dark") finally: root.destroy() def test_falls_back_to_light_when_none(self): import tkinter as tk from unittest.mock import patch from whisper_local.tray._theme import apply_system_theme root = tk.Tk() root.withdraw() try: with patch("darkdetect.theme", return_value=None), \ patch("sv_ttk.set_theme") as mock_set: apply_system_theme(root) mock_set.assert_called_once_with("light") finally: root.destroy() @pytest.mark.skipif(sys.platform != "win32", reason="Settings nur auf Windows") class TestCheckHotkeyConflict: def test_returns_false_when_key_is_free(self): from unittest.mock import patch, MagicMock from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict mock_user32 = MagicMock() mock_user32.RegisterHotKey.return_value = 1 # Erfolg with patch("ctypes.windll") as mock_windll: mock_windll.user32 = mock_user32 result = check_hotkey_conflict("KEY_F12") assert result is False mock_user32.UnregisterHotKey.assert_called_once() def test_returns_true_when_key_is_taken(self): from unittest.mock import patch, MagicMock from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict mock_user32 = MagicMock() mock_user32.RegisterHotKey.return_value = 0 # Belegt with patch("ctypes.windll") as mock_windll: mock_windll.user32 = mock_user32 result = check_hotkey_conflict("KEY_F12") assert result is True mock_user32.UnregisterHotKey.assert_not_called() def test_returns_false_for_unknown_key(self): from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict result = check_hotkey_conflict("KEY_NONEXISTENT_999") assert result is False class TestListMicrophones: def test_returns_only_input_devices(self): from unittest.mock import patch from whisper_local.tray._settings import list_microphones fake_devices = [ {"name": "Speakers", "max_input_channels": 0}, {"name": "Headset Mic", "max_input_channels": 1}, {"name": "USB Mic", "max_input_channels": 2}, ] with patch("sounddevice.query_devices", return_value=fake_devices): result = list_microphones() assert result == [("Headset Mic", 1), ("USB Mic", 2)] def test_returns_empty_list_when_no_input(self): from unittest.mock import patch from whisper_local.tray._settings import list_microphones with patch("sounddevice.query_devices", return_value=[]): result = list_microphones() assert result == [] class TestPynputToEvdevKey: def test_function_key(self): from pynput.keyboard import Key from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key assert pynput_to_evdev_key(Key.f12) == "KEY_F12" def test_space_key(self): from pynput.keyboard import Key from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key assert pynput_to_evdev_key(Key.space) == "KEY_SPACE" def test_char_key(self): from pynput.keyboard import KeyCode from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key key = KeyCode.from_char("a") assert pynput_to_evdev_key(key) == "KEY_A" def test_unknown_returns_empty(self): from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key assert pynput_to_evdev_key(None) == "" @pytest.mark.skipif(sys.platform != "win32", reason="Settings nur auf Windows") class TestSettingsDialog: def test_on_save_called_with_new_config(self): import tkinter as tk from unittest.mock import patch, MagicMock, call from whisper_local.config import Config from whisper_local.tray._settings import SettingsDialog saved = [] dialog = SettingsDialog( config=Config(hotkey="KEY_F12", microphone=""), on_save=saved.append, ) # Dialog direkt aufrufen (nicht in Thread), mit gemocktem mainloop with patch("tkinter.Tk.mainloop"), \ patch("whisper_local.tray._settings.apply_system_theme"), \ patch("whisper_local.tray._settings.list_microphones", return_value=[]), \ patch("whisper_local.tray._settings.save_config") as mock_save: dialog._run() # _run() ruft mainloop auf, der sofort zurückkehrt. # save() wird nicht automatisch aufgerufen — das ist korrekt. # Wir prüfen nur, dass _run() ohne Fehler durchläuft. assert mock_save.call_count == 0 # Noch nicht gespeichert ohne Klick def test_on_save_callback_called_when_save_invoked(self): import tkinter as tk from unittest.mock import patch, MagicMock from whisper_local.config import Config from whisper_local.tray._settings import SettingsDialog saved_configs = [] dialog = SettingsDialog( config=Config(hotkey="KEY_F12", microphone=""), on_save=saved_configs.append, ) # _run() intern aufrufen und save() direkt triggern captured_save_fn = [] def fake_button(frame, text, command, **kwargs): if text == "Speichern": captured_save_fn.append(command) mock = MagicMock() mock.pack = MagicMock() return mock with patch("tkinter.Tk.mainloop"), \ patch("whisper_local.tray._settings.apply_system_theme"), \ patch("whisper_local.tray._settings.list_microphones", return_value=[("USB Mic", 0)]), \ patch("whisper_local.tray._settings.save_config"), \ patch("tkinter.ttk.Button", side_effect=fake_button): dialog._run() if captured_save_fn: captured_save_fn[0]() assert len(saved_configs) == 1 class TestCreateTray: @pytest.mark.skipif(sys.platform != "win32", reason="PystrayApp nur auf Windows") def test_returns_pystray_on_windows(self): from unittest.mock import MagicMock from whisper_local.tray import create_tray from whisper_local.tray._tray import PystrayApp tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock()) assert isinstance(tray, PystrayApp) @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only") def test_returns_pystray_on_linux(self): from unittest.mock import MagicMock from whisper_local.tray import create_tray from whisper_local.tray._tray import PystrayApp tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock()) assert isinstance(tray, PystrayApp) @pytest.mark.skipif( sys.platform in ("win32", "linux"), reason="NoOpTray nur auf Plattformen ohne Tray-Unterstützung", ) def test_returns_noop_tray_on_unsupported_platform(self): from unittest.mock import MagicMock from whisper_local.tray import create_tray from whisper_local.tray._tray import NoOpTray tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock()) assert isinstance(tray, NoOpTray) def test_appstate_exported_from_package(self): from whisper_local.tray import AppState assert AppState.WAITING is not None assert AppState.RECORDING is not None assert AppState.TRANSCRIBING is not None @pytest.mark.skipif(sys.platform != "linux", reason="evdev-Record nur auf Linux") class TestRecordHotkeyEvdev: def test_first_keydown_triggers_on_result(self): import threading from unittest.mock import MagicMock, patch fake_event = MagicMock() fake_event.type = 1 # EV_KEY fake_event.code = 88 # KEY_F12 fake_event.value = 1 # Keydown fake_device = MagicMock() fake_device.fd = 42 fake_device.read.return_value = iter([fake_event]) fake_device.close = MagicMock() cancel = threading.Event() results: list[tuple[str, bool]] = [] def on_result(name, conflict): results.append((name, conflict)) with patch( "whisper_local.tray._hotkey_record_evdev.find_all_keyboards", return_value=[fake_device], ), patch( "whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector" ) as mock_sel_cls: mock_sel = MagicMock() mock_sel_cls.return_value = mock_sel sel_key = MagicMock() sel_key.data = fake_device mock_sel.select.return_value = [(sel_key, None)] from whisper_local.tray._hotkey_record_evdev import record_hotkey record_hotkey(on_result, cancel) assert results == [("KEY_F12", False)] fake_device.close.assert_called_once() def test_cancel_event_stops_recording(self): import threading from unittest.mock import MagicMock, patch fake_device = MagicMock() fake_device.fd = 42 fake_device.read.return_value = iter([]) fake_device.close = MagicMock() cancel = threading.Event() cancel.set() results: list[tuple[str, bool]] = [] with patch( "whisper_local.tray._hotkey_record_evdev.find_all_keyboards", return_value=[fake_device], ), patch( "whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector" ) as mock_sel_cls: mock_sel = MagicMock() mock_sel_cls.return_value = mock_sel mock_sel.select.return_value = [] from whisper_local.tray._hotkey_record_evdev import record_hotkey record_hotkey(lambda n, c: results.append((n, c)), cancel) assert results == [] fake_device.close.assert_called_once() def test_ignores_key_up_events(self): import threading from unittest.mock import MagicMock, patch key_up = MagicMock() key_up.type = 1 key_up.code = 88 key_up.value = 0 key_down = MagicMock() key_down.type = 1 key_down.code = 88 key_down.value = 1 fake_device = MagicMock() fake_device.fd = 42 fake_device.read.return_value = iter([key_up, key_down]) fake_device.close = MagicMock() cancel = threading.Event() results: list[tuple[str, bool]] = [] with patch( "whisper_local.tray._hotkey_record_evdev.find_all_keyboards", return_value=[fake_device], ), patch( "whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector" ) as mock_sel_cls: mock_sel = MagicMock() mock_sel_cls.return_value = mock_sel sel_key = MagicMock() sel_key.data = fake_device mock_sel.select.return_value = [(sel_key, None)] from whisper_local.tray._hotkey_record_evdev import record_hotkey record_hotkey(lambda n, c: results.append((n, c)), cancel) assert results == [("KEY_F12", False)] def test_skips_unknown_keycodes(self): import threading from unittest.mock import MagicMock, patch unknown_event = MagicMock() unknown_event.type = 1 # EV_KEY unknown_event.code = 9999 # unbekannter Keycode unknown_event.value = 1 # Keydown known_event = MagicMock() known_event.type = 1 # EV_KEY known_event.code = 88 # KEY_F12 known_event.value = 1 # Keydown fake_device = MagicMock() fake_device.fd = 42 fake_device.read.return_value = iter([unknown_event, known_event]) fake_device.close = MagicMock() cancel = threading.Event() results: list[tuple[str, bool]] = [] with patch( "whisper_local.tray._hotkey_record_evdev.find_all_keyboards", return_value=[fake_device], ), patch( "whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector" ) as mock_sel_cls: mock_sel = MagicMock() mock_sel_cls.return_value = mock_sel sel_key = MagicMock() sel_key.data = fake_device mock_sel.select.return_value = [(sel_key, None)] from whisper_local.tray._hotkey_record_evdev import record_hotkey record_hotkey(lambda n, c: results.append((n, c)), cancel) assert results == [("KEY_F12", False)]