2026-06-22 07:33:49 +02:00
|
|
|
"""Tests for WakeAlarm's gatelock hooks: on_focus_ready, on_callback_error, on_close."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from collections.abc import Generator
|
|
|
|
|
|
2026-06-22 12:31:40 +02:00
|
|
|
from wake_alarm._alarm import WakeAlarm
|
2026-06-22 07:33:49 +02:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Helpers (duplicated from part 1 so this file is self-contained)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_mock_tk() -> MagicMock:
|
|
|
|
|
"""Build a MagicMock that stands in for the tkinter module."""
|
|
|
|
|
mock = MagicMock()
|
|
|
|
|
mock_root = MagicMock()
|
|
|
|
|
mock_root.winfo_screenwidth.return_value = 1920
|
|
|
|
|
mock_root.winfo_screenheight.return_value = 1080
|
|
|
|
|
mock.Tk.return_value = mock_root
|
|
|
|
|
mock.Frame.return_value = MagicMock()
|
|
|
|
|
mock.Label.return_value = MagicMock()
|
|
|
|
|
mock.Entry.return_value = MagicMock()
|
|
|
|
|
mock.TclError = tk.TclError
|
|
|
|
|
mock.END = tk.END
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _block_real_tk() -> Generator[MagicMock]:
|
|
|
|
|
"""Prevent any real Tk windows in tests."""
|
|
|
|
|
mock = _make_mock_tk()
|
|
|
|
|
with (
|
2026-06-22 12:31:40 +02:00
|
|
|
patch("wake_alarm._alarm.tk", mock),
|
2026-06-22 07:33:49 +02:00
|
|
|
patch(
|
2026-06-22 12:31:40 +02:00
|
|
|
"wake_alarm._alarm.GateRoot",
|
2026-06-22 07:33:49 +02:00
|
|
|
return_value=mock.Tk.return_value,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
yield mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _block_extra_devices() -> Generator[MagicMock]:
|
|
|
|
|
"""Prevent real subprocess.Popen calls for extra ALSA devices."""
|
|
|
|
|
with (
|
2026-06-22 12:31:40 +02:00
|
|
|
patch("wake_alarm._alarm._play_on_extra_devices") as mock,
|
|
|
|
|
patch("wake_alarm._alarm._max_fans", return_value=False),
|
|
|
|
|
patch("wake_alarm._alarm._restore_fans"),
|
|
|
|
|
patch("wake_alarm._alarm._set_max_brightness"),
|
|
|
|
|
patch("wake_alarm._alarm._wake_display"),
|
|
|
|
|
patch("wake_alarm._alarm._restore_display"),
|
|
|
|
|
patch("wake_alarm._alarm._warn_if_no_real_sink"),
|
|
|
|
|
patch("wake_alarm._alarm._activate_alarm_audio", return_value=None),
|
|
|
|
|
patch("wake_alarm._alarm._restore_alarm_audio"),
|
|
|
|
|
patch("wake_alarm._alarm.turn_on_plug"),
|
|
|
|
|
patch("wake_alarm._alarm.turn_off_plug"),
|
2026-06-22 07:33:49 +02:00
|
|
|
):
|
|
|
|
|
yield mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_tk_module() -> Generator[MagicMock]:
|
|
|
|
|
"""Provide explicit access to the mocked tk module."""
|
|
|
|
|
mock = _make_mock_tk()
|
|
|
|
|
with (
|
2026-06-22 12:31:40 +02:00
|
|
|
patch("wake_alarm._alarm.tk", mock),
|
2026-06-22 07:33:49 +02:00
|
|
|
patch(
|
2026-06-22 12:31:40 +02:00
|
|
|
"wake_alarm._alarm.GateRoot",
|
2026-06-22 07:33:49 +02:00
|
|
|
return_value=mock.Tk.return_value,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
yield mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGatelockHooks:
|
|
|
|
|
"""Tests for the LockWindowHooks callbacks (on_focus_ready/on_callback_error)."""
|
|
|
|
|
|
|
|
|
|
def test_on_focus_ready_focuses_entry(
|
|
|
|
|
self,
|
|
|
|
|
mock_tk_module: MagicMock,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""on_focus_ready forces focus onto the dismiss-code entry."""
|
|
|
|
|
del mock_tk_module
|
|
|
|
|
alarm = WakeAlarm(demo_mode=True)
|
|
|
|
|
alarm._view.entry.focus_force.reset_mock()
|
|
|
|
|
alarm.on_focus_ready()
|
|
|
|
|
alarm._view.entry.focus_force.assert_called_once()
|
|
|
|
|
alarm._stop_beep.set()
|
|
|
|
|
|
|
|
|
|
def test_on_callback_error_surfaces_and_refocuses(
|
|
|
|
|
self,
|
|
|
|
|
mock_tk_module: MagicMock,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""on_callback_error shows a message and refocuses the entry."""
|
|
|
|
|
del mock_tk_module
|
|
|
|
|
alarm = WakeAlarm(demo_mode=True)
|
|
|
|
|
alarm._view.entry.focus_force.reset_mock()
|
|
|
|
|
alarm.on_callback_error()
|
|
|
|
|
alarm._view.status_label.configure.assert_called_with(
|
|
|
|
|
text="Something went wrong — try again.",
|
|
|
|
|
)
|
|
|
|
|
alarm._view.entry.focus_force.assert_called_once()
|
|
|
|
|
alarm._stop_beep.set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestClose:
|
|
|
|
|
"""Tests for the alarm's gatelock close path (LockWindow.close/on_close)."""
|
|
|
|
|
|
|
|
|
|
def test_lock_close_stops_beep_and_destroys(
|
|
|
|
|
self,
|
|
|
|
|
mock_tk_module: MagicMock,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""LockWindow.close() runs on_close (stop event) and destroys root."""
|
|
|
|
|
del mock_tk_module
|
|
|
|
|
alarm = WakeAlarm(demo_mode=True)
|
|
|
|
|
alarm._lock.close()
|
|
|
|
|
assert alarm._stop_beep.is_set()
|
|
|
|
|
alarm.root.destroy.assert_called()
|
|
|
|
|
|
|
|
|
|
def test_on_close_restores_fans(
|
|
|
|
|
self,
|
|
|
|
|
mock_tk_module: MagicMock,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""on_close calls _restore_fans with the saved fan state."""
|
|
|
|
|
del mock_tk_module
|
|
|
|
|
alarm = WakeAlarm(demo_mode=True)
|
|
|
|
|
alarm._hardware.fan_state = True
|
2026-06-22 12:31:40 +02:00
|
|
|
with patch("wake_alarm._alarm._restore_fans") as mock_restore:
|
2026-06-22 07:33:49 +02:00
|
|
|
alarm.on_close()
|
|
|
|
|
mock_restore.assert_called_once_with(active=True)
|
|
|
|
|
alarm._stop_beep.set()
|
|
|
|
|
|
|
|
|
|
def test_on_close_restores_audio(
|
|
|
|
|
self,
|
|
|
|
|
mock_tk_module: MagicMock,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""on_close restores the default sink captured at activation."""
|
|
|
|
|
del mock_tk_module
|
|
|
|
|
alarm = WakeAlarm(demo_mode=True)
|
|
|
|
|
alarm._hardware.audio_restore = "jbl_sink"
|
|
|
|
|
with patch(
|
2026-06-22 12:31:40 +02:00
|
|
|
"wake_alarm._alarm._restore_alarm_audio",
|
2026-06-22 07:33:49 +02:00
|
|
|
) as mock_restore:
|
|
|
|
|
alarm.on_close()
|
|
|
|
|
mock_restore.assert_called_once_with("jbl_sink")
|
|
|
|
|
alarm._stop_beep.set()
|