"""Tests for UI transitions, timer logic, and the retry screen.""" 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 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