wake-alarm/wake_alarm/tests/test_alarm_part4.py
Krzysztof kuhy Rudnicki 407d7cbf8f Extract wake_alarm from testsAndMisc as a standalone repo
Rewrites python_pkg.wake_alarm imports to wake_alarm, vendors the
shared configure_logging helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
untracks wake_state.json (runtime HMAC state, now gitignored). Scaffolds
standalone lint/test config copied from the already-corrected diet_guard
scaffold (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage), plus
the wave.Wave_write generated-members fix this package's _audio.py needs.
2026-06-22 12:31:40 +02:00

155 lines
5.0 KiB
Python

"""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
from wake_alarm._alarm import WakeAlarm
# ---------------------------------------------------------------------------
# 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 (
patch("wake_alarm._alarm.tk", mock),
patch(
"wake_alarm._alarm.GateRoot",
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 (
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"),
):
yield mock
@pytest.fixture
def mock_tk_module() -> Generator[MagicMock]:
"""Provide explicit access to the mocked tk module."""
mock = _make_mock_tk()
with (
patch("wake_alarm._alarm.tk", mock),
patch(
"wake_alarm._alarm.GateRoot",
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
with patch("wake_alarm._alarm._restore_fans") as mock_restore:
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(
"wake_alarm._alarm._restore_alarm_audio",
) as mock_restore:
alarm.on_close()
mock_restore.assert_called_once_with("jbl_sink")
alarm._stop_beep.set()