mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 12:07:44 +01:00
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
The previous session reported the new code green, but measured it with --no-cov (step 1 of finish_2026_08_24.sh passes that flag), so seven branches in the recovery path had never been exercised. Coverage was 99.78%. Most of the gap was the auto-recovery itself: whether healing succeeded, whether the retry after healing succeeded, and what gets recorded when it does not. That is the code which ran in production for the first time today, and none of its outcomes were pinned. Also covers the donor search on a machine with no ~/.config, siblings whose cache is readable but holds no usable token, and the two healthy paths that must stay silent — an always-on "this may be wrong" panel is one nobody reads. Writing the UI test surfaced that collect_source_findings is never empty for a fresh tmp_path, because an empty sync is itself a finding; the stub is there so the branch under test is "nothing to report". 1115 passed, coverage back to 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJmPLyT8NU4DzDJPV1KEX5
228 lines
7.5 KiB
Python
228 lines
7.5 KiB
Python
"""Tests for UI transitions, timer logic, and sick day screens."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock
|
|
|
|
from screen_locker import _sync_client, _ui_flows
|
|
from screen_locker.tests.conftest import create_locker
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
class TestUITransitions:
|
|
"""Tests for UI state transitions."""
|
|
|
|
def test_clear_container(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test clear_container destroys all child widgets."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
|
|
# Set up mock children
|
|
mock_child1 = MagicMock()
|
|
mock_child2 = MagicMock()
|
|
locker.container.first.winfo_children.return_value = [
|
|
mock_child1,
|
|
mock_child2,
|
|
]
|
|
|
|
locker.clear_container()
|
|
|
|
mock_child1.destroy.assert_called_once()
|
|
mock_child2.destroy.assert_called_once()
|
|
|
|
def test_unlock_screen_saves_and_schedules_close(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test unlock_screen saves log and schedules close."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker.log_file = tmp_path / "log.json"
|
|
locker.workout_data = {"type": "phone_verified"}
|
|
|
|
locker.unlock_screen()
|
|
|
|
# Check that after() was called to schedule close
|
|
locker.root.after.assert_called()
|
|
|
|
def test_lockout_starts_countdown(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test lockout initializes countdown timer."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
|
|
locker.lockout()
|
|
|
|
# lockout() sets remaining_time to lockout_time (10 in demo mode)
|
|
# then calls update_lockout_countdown() which decrements it by 1
|
|
assert locker.remaining_time == 9 # 10 - 1 after first update
|
|
|
|
def test_close_destroys_root_and_exits(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test close destroys root window and exits."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
|
|
locker.close()
|
|
|
|
locker.root.destroy.assert_called_once()
|
|
mock_sys_exit.assert_called_with(0)
|
|
|
|
|
|
class TestTimerLogic:
|
|
"""Tests for timer countdown logic."""
|
|
|
|
def test_update_lockout_countdown_decrements(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test countdown decrements remaining time."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker.remaining_time = 5
|
|
locker.countdown_label = MagicMock()
|
|
|
|
locker.update_lockout_countdown()
|
|
|
|
assert locker.remaining_time == 4
|
|
locker.root.after.assert_called_with(1000, locker.update_lockout_countdown)
|
|
|
|
def test_update_lockout_countdown_at_zero(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test countdown at zero restarts phone check."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker.remaining_time = 0
|
|
locker.countdown_label = MagicMock()
|
|
object.__setattr__(locker, "_start_phone_check", MagicMock())
|
|
|
|
locker.update_lockout_countdown()
|
|
|
|
locker._start_phone_check.assert_called_once()
|
|
|
|
|
|
class TestAskIfSick:
|
|
"""Tests for ask_if_sick method."""
|
|
|
|
def test_ask_if_sick_invokes_justification_dialog(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""ask_if_sick now delegates to the structured justification dialog."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(locker, "_show_sick_justification", MagicMock())
|
|
locker.ask_if_sick()
|
|
locker._show_sick_justification.assert_called_once_with()
|
|
|
|
|
|
class TestGetSickDayStatus:
|
|
"""Tests for _get_sick_day_status method."""
|
|
|
|
def test_already_adjusted_today(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""Test status when sick mode already used today."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(
|
|
locker, "_sick_mode_used_today", MagicMock(return_value=True)
|
|
)
|
|
text, color = locker._get_sick_day_status()
|
|
assert "already adjusted" in text
|
|
assert color == locker._colors.warning
|
|
|
|
def test_adjustment_success(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""Test status when shutdown time adjusted successfully."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(
|
|
locker, "_sick_mode_used_today", MagicMock(return_value=False)
|
|
)
|
|
object.__setattr__(
|
|
locker, "_adjust_shutdown_time_earlier", MagicMock(return_value=True)
|
|
)
|
|
text, color = locker._get_sick_day_status()
|
|
assert "earlier" in text
|
|
assert color == locker._colors.success
|
|
|
|
def test_adjustment_failure(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""Test status when adjustment fails."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(
|
|
locker, "_sick_mode_used_today", MagicMock(return_value=False)
|
|
)
|
|
object.__setattr__(
|
|
locker, "_adjust_shutdown_time_earlier", MagicMock(return_value=False)
|
|
)
|
|
text, color = locker._get_sick_day_status()
|
|
assert "Could not adjust" in text
|
|
assert color == locker._colors.danger
|
|
|
|
|
|
class TestShowRetryAndSick:
|
|
"""Tests for _show_retry_and_sick method."""
|
|
|
|
def test_displays_buttons(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""Test _show_retry_and_sick shows retry and sick buttons."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(locker, "clear_container", MagicMock())
|
|
|
|
locker._show_retry_and_sick("Test message")
|
|
|
|
locker.clear_container.assert_called_once()
|
|
mock_tk.Label.assert_called()
|
|
mock_tk.Button.assert_called()
|
|
|
|
def test_healthy_sources_add_no_diagnosis_block(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""With every source healthy the screen must not hedge.
|
|
|
|
The "why this may be wrong" panel exists for 2026-08-24, when the
|
|
machine genuinely could not see the workout. When it *can* see, an
|
|
unconditional caveat would teach the user to ignore the one message
|
|
that matters.
|
|
"""
|
|
_sync_client.clear_degraded_sources()
|
|
# A healthy machine has a readable backend AND a non-empty log, which
|
|
# this tmp_path fixture cannot provide -- an empty sync is itself a
|
|
# finding. Stub the collector so the branch under test is "nothing to
|
|
# report", not "no workouts synced".
|
|
monkeypatch.setattr(_ui_flows, "collect_source_findings", list)
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(locker, "clear_container", MagicMock())
|
|
|
|
locker._show_retry_and_sick("Test message")
|
|
|
|
shown = " ".join(
|
|
str(call.kwargs.get("text", "")) for call in mock_tk.Label.call_args_list
|
|
)
|
|
assert "may be wrong" not in shown
|