2026-04-10 21:07:48 +02:00
|
|
|
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()
|
2026-04-10 21:09:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows")
|
2026-04-11 21:09:39 +02:00
|
|
|
class TestPystrayApp:
|
2026-04-10 21:09:10 +02:00
|
|
|
def test_set_state_updates_icon(self):
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
2026-04-11 21:09:39 +02:00
|
|
|
from whisper_local.tray._tray import AppState, PystrayApp
|
2026-04-10 21:09:10 +02:00
|
|
|
|
2026-04-11 21:09:39 +02:00
|
|
|
app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
2026-04-10 21:09:10 +02:00
|
|
|
|
|
|
|
|
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
|
2026-04-11 21:09:39 +02:00
|
|
|
from whisper_local.tray._tray import AppState, PystrayApp
|
2026-04-10 21:09:10 +02:00
|
|
|
|
2026-04-11 21:09:39 +02:00
|
|
|
app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
2026-04-10 21:09:10 +02:00
|
|
|
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
|
2026-04-10 21:11:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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()
|
2026-04-10 21:13:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
2026-04-10 21:13:00 +02:00
|
|
|
|
|
|
|
|
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
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
2026-04-10 21:13:00 +02:00
|
|
|
|
|
|
|
|
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):
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
2026-04-10 21:13:00 +02:00
|
|
|
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
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
2026-04-10 21:13:00 +02:00
|
|
|
assert pynput_to_evdev_key(Key.f12) == "KEY_F12"
|
|
|
|
|
|
|
|
|
|
def test_space_key(self):
|
|
|
|
|
from pynput.keyboard import Key
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
2026-04-10 21:13:00 +02:00
|
|
|
assert pynput_to_evdev_key(Key.space) == "KEY_SPACE"
|
|
|
|
|
|
|
|
|
|
def test_char_key(self):
|
|
|
|
|
from pynput.keyboard import KeyCode
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
2026-04-10 21:13:00 +02:00
|
|
|
key = KeyCode.from_char("a")
|
|
|
|
|
assert pynput_to_evdev_key(key) == "KEY_A"
|
|
|
|
|
|
|
|
|
|
def test_unknown_returns_empty(self):
|
2026-04-11 21:13:10 +02:00
|
|
|
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
2026-04-10 21:13:00 +02:00
|
|
|
assert pynput_to_evdev_key(None) == ""
|
2026-04-10 21:15:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-04-10 21:17:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCreateTray:
|
2026-04-11 21:09:39 +02:00
|
|
|
@pytest.mark.skipif(sys.platform != "win32", reason="PystrayApp nur auf Windows")
|
|
|
|
|
def test_returns_pystray_on_windows(self):
|
2026-04-10 21:17:05 +02:00
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
from whisper_local.tray import create_tray
|
2026-04-11 21:09:39 +02:00
|
|
|
from whisper_local.tray._tray import PystrayApp
|
2026-04-10 21:17:05 +02:00
|
|
|
|
|
|
|
|
tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock())
|
2026-04-11 21:09:39 +02:00
|
|
|
assert isinstance(tray, PystrayApp)
|
2026-04-10 21:17:05 +02:00
|
|
|
|
2026-04-11 21:30:19 +02:00
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="Linux-only")
|
|
|
|
|
def test_returns_pystray_on_linux(self):
|
2026-04-11 21:32:49 +02:00
|
|
|
from unittest.mock import MagicMock
|
2026-04-11 21:30:19 +02:00
|
|
|
from whisper_local.tray import create_tray
|
|
|
|
|
from whisper_local.tray._tray import PystrayApp
|
|
|
|
|
|
2026-04-11 21:32:49 +02:00
|
|
|
tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock())
|
2026-04-11 21:30:19 +02:00
|
|
|
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):
|
2026-04-10 21:17:05 +02:00
|
|
|
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
|
2026-04-11 21:16:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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)]
|
2026-04-11 21:19:07 +02:00
|
|
|
|
|
|
|
|
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)]
|