Files
whisper-local/tests/test_hotkey.py
T
Vitali Graf 6c678fbcfb fix: suppress key-repeat events in pynput hotkey listener
Holding the hotkey caused dozens of on_press callbacks due to OS key-repeat.
Added a _pressed guard flag so only the first press and actual release fire.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 10:53:09 +02:00

115 lines
4.0 KiB
Python

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