Files
whisper-local/tests/test_tray.py
T

271 lines
10 KiB
Python

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 == "win32", reason="NoOpTray nur auf nicht-Windows")
def test_returns_noop_tray_on_non_windows(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