import asyncio import sys from unittest.mock import AsyncMock, MagicMock, patch import pytest @pytest.fixture def listener(): from whisper_local.hotkey._evdev import EvdevHotkeyListener return EvdevHotkeyListener(key_name="KEY_F12") @pytest.mark.skipif(sys.platform != "linux", reason="evdev only available on Linux") class TestHotkeyListener: def test_init_stores_key_name(self, listener): assert listener.key_name == "KEY_F12" @pytest.mark.asyncio async def test_key_down_calls_callback(self, listener): on_press = AsyncMock() listener.on_press = on_press await listener._handle_key_event(key_down=True) on_press.assert_awaited_once() @pytest.mark.asyncio async def test_key_up_calls_callback(self, listener): on_release = AsyncMock() listener.on_release = on_release await listener._handle_key_event(key_down=False) on_release.assert_awaited_once() @pytest.mark.asyncio async def test_no_callback_no_error(self, listener): await listener._handle_key_event(key_down=True) await listener._handle_key_event(key_down=False) class TestPynputKeyMapping: """Tests für die Key-Name-Übersetzung von evdev zu pynput.""" def test_map_function_key(self): from whisper_local.hotkey._pynput import _evdev_to_pynput_key from pynput.keyboard import Key assert _evdev_to_pynput_key("KEY_F12") == Key.f12 def test_map_f1(self): from whisper_local.hotkey._pynput import _evdev_to_pynput_key from pynput.keyboard import Key assert _evdev_to_pynput_key("KEY_F1") == Key.f1 def test_map_unknown_key_raises(self): from whisper_local.hotkey._pynput import _evdev_to_pynput_key with pytest.raises(ValueError, match="Unbekannter Key-Name"): _evdev_to_pynput_key("KEY_NONEXISTENT_999") class TestPynputHotkeyListener: def test_init_stores_key_name(self): from whisper_local.hotkey._pynput import PynputHotkeyListener listener = PynputHotkeyListener("KEY_F12") assert listener.key_name == "KEY_F12" @pytest.mark.asyncio async def test_handle_key_event_press(self): from whisper_local.hotkey._pynput import PynputHotkeyListener listener = PynputHotkeyListener("KEY_F12") on_press = AsyncMock() listener.on_press = on_press await listener._handle_key_event(key_down=True) on_press.assert_awaited_once() @pytest.mark.asyncio async def test_handle_key_event_release(self): from whisper_local.hotkey._pynput import PynputHotkeyListener listener = PynputHotkeyListener("KEY_F12") on_release = AsyncMock() listener.on_release = on_release await listener._handle_key_event(key_down=False) on_release.assert_awaited_once() @pytest.mark.asyncio async def test_no_callback_no_error(self): from whisper_local.hotkey._pynput import PynputHotkeyListener listener = PynputHotkeyListener("KEY_F12") await listener._handle_key_event(key_down=True) await listener._handle_key_event(key_down=False) def test_key_repeat_ignored(self): from whisper_local.hotkey._pynput import PynputHotkeyListener from pynput.keyboard import Key listener = PynputHotkeyListener("KEY_F12") listener._loop = MagicMock() # Erstes Press — wird durchgelassen listener._on_press(Key.f12) assert listener._loop.call_soon_threadsafe.call_count == 1 # Wiederholte Presses (Key-Repeat) — werden ignoriert listener._on_press(Key.f12) listener._on_press(Key.f12) assert listener._loop.call_soon_threadsafe.call_count == 1 # Release — wird durchgelassen listener._on_release(Key.f12) assert listener._loop.call_soon_threadsafe.call_count == 2 # Erneutes Release ohne Press — wird ignoriert listener._on_release(Key.f12) assert listener._loop.call_soon_threadsafe.call_count == 2 class TestPynputHotkeyListenerStop: @pytest.mark.asyncio async def test_stop_ends_listen(self): from whisper_local.hotkey._pynput import PynputHotkeyListener from unittest.mock import patch, MagicMock listener = PynputHotkeyListener("KEY_F12") with patch("whisper_local.hotkey._pynput.Listener") as mock_listener_cls: mock_listener_cls.return_value = MagicMock() listen_task = asyncio.create_task(listener.listen()) await asyncio.sleep(0) # Loop einen Schritt weiter listener.stop() await asyncio.wait_for(listen_task, timeout=1.0)