screen-locker/screen_locker/tests/test_manual_workout_date.py
Krzysztof kuhy Rudnicki aa3e290391 Document every test, split files at the cap
Real one-line docstrings stating what each test pins down. Files that
would pass 250 lines moved whole classes into _part2 siblings
(sick_panel, earned, poke_server, shutdown_reading,
manual_workout_dialog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:11:34 +01:00

165 lines
7.2 KiB
Python

"""Tests for the workout-date field and the future-dated submit path."""
from __future__ import annotations
from datetime import date, timedelta
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker._day import today_str
from screen_locker._manual_schedule import ScheduleResult
from screen_locker.tests import test_manual_workout_dialog_part2 as dialog_tests
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
_MOD = "screen_locker._manual_workout_date"
def _plus(days: int) -> str:
"""Return the local ISO date ``days`` from today."""
return (date.fromisoformat(today_str()) + timedelta(days=days)).isoformat()
def _locker(mock_tk: MagicMock, tmp_path: Path, day: str) -> MagicMock:
"""A locker whose form already holds ``day`` in its date variable."""
locker = create_locker(mock_tk, tmp_path)
locker._mw_vars = {"date": MagicMock()}
locker._mw_vars["date"].get.return_value = day
locker._mw_error_label = MagicMock()
locker._show_manual_workout_scheduled = MagicMock()
return locker
class TestBudgetClosed:
"""The button gate reads the local ledger only (no network)."""
def test_open_with_an_empty_log(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""With an empty log the budget is not closed."""
locker = create_locker(mock_tk, tmp_path)
assert locker._mw_budget_closed() is False
class TestDateInRow:
"""The date entry rides in the sport row, prefilled with today."""
def test_prefills_today_and_registers_the_var(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""The date entry is prefilled with today and registered in the form vars."""
locker = create_locker(mock_tk, tmp_path)
locker._mw_vars = {}
locker._mw_date_in_row(MagicMock())
assert locker._mw_vars["date"] is mock_tk.StringVar.return_value
assert mock_tk.StringVar.call_args.kwargs["value"] == today_str()
class TestHandledByDate:
"""Date and budget rules applied at submit."""
def test_a_garbled_date_shows_the_error(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""An unparsable date shows an error and schedules nothing."""
locker = _locker(mock_tk, tmp_path, "soon")
assert locker._mw_handled_by_date(MagicMock()) is True
locker._mw_error_label.config.assert_called_once()
locker._show_manual_workout_scheduled.assert_not_called()
def test_a_past_date_shows_the_error(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""A date before today is rejected with a message mentioning the past."""
locker = _locker(mock_tk, tmp_path, _plus(-1))
assert locker._mw_handled_by_date(MagicMock()) is True
assert "past" in locker._mw_error_label.config.call_args.kwargs["text"]
def test_today_over_budget_shows_the_error(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Today's date with no budget left shows the budget error and is handled."""
locker = _locker(mock_tk, tmp_path, today_str())
with patch(f"{_MOD}.budget_error", return_value="budget full"):
assert locker._mw_handled_by_date(MagicMock()) is True
locker._mw_error_label.config.assert_called_once_with(text="budget full")
def test_today_within_budget_is_left_to_the_host(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Today's date within budget is not handled and shows no error."""
locker = _locker(mock_tk, tmp_path, today_str())
with patch(f"{_MOD}.budget_error", return_value=None):
assert locker._mw_handled_by_date(MagicMock()) is False
locker._mw_error_label.config.assert_not_called()
def test_a_refused_future_entry_shows_why(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""A future entry the scheduler refuses shows its reason and no confirmation."""
locker = _locker(mock_tk, tmp_path, _plus(2))
refused = ScheduleResult(scheduled=False, record_id="r", reason="no room")
with patch(f"{_MOD}.schedule_future_workout", return_value=refused):
assert locker._mw_handled_by_date(MagicMock()) is True
locker._mw_error_label.config.assert_called_once_with(text="no room")
locker._show_manual_workout_scheduled.assert_not_called()
def test_a_scheduled_future_entry_shows_the_confirmation(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""A scheduled future entry is saved via the scheduler and confirmed without error."""
locker = _locker(mock_tk, tmp_path, _plus(2))
draft = MagicMock()
done = ScheduleResult(scheduled=True, record_id="r", reason="scheduled")
with patch(f"{_MOD}.schedule_future_workout", return_value=done) as schedule:
assert locker._mw_handled_by_date(draft) is True
schedule.assert_called_once_with(locker.log_file, draft, _plus(2))
locker._show_manual_workout_scheduled.assert_called_once_with("scheduled")
locker._mw_error_label.config.assert_not_called()
class TestScheduledConfirmation:
"""The confirmation must go BACK, never through the unlocking save hook."""
def test_ok_returns_via_the_cancelled_hook_and_never_unlocks(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""The OK button goes back through the cancelled hook and never the saved one."""
locker = create_locker(mock_tk, tmp_path)
saved = MagicMock()
cancelled = MagicMock()
object.__setattr__(locker, "_on_manual_workout_saved", saved)
object.__setattr__(locker, "_on_manual_workout_cancelled", cancelled)
mock_tk.Button.reset_mock()
locker._show_manual_workout_scheduled("scheduled for tomorrow")
buttons = {
c.kwargs.get("text"): c.kwargs for c in mock_tk.Button.call_args_list
}
assert set(buttons) == {"OK"}
buttons["OK"]["command"]()
cancelled.assert_called_once_with()
saved.assert_not_called()
texts = [c.kwargs.get("text", "") for c in mock_tk.Label.call_args_list]
assert any("Workout Scheduled" in t for t in texts)
assert any("scheduled for tomorrow" in t for t in texts)
class TestSubmitRoutesThroughTheDateRules:
"""The dialog's submit stops when the date rules handled the entry."""
def test_a_handled_entry_never_reaches_the_host_save_hook(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""When the date rules handle the entry the host save hook is not called."""
locker = dialog_tests.TestSubmitManualWorkoutForm()._setup_locker(
mock_tk, tmp_path
)
locker._mw_handled_by_date = MagicMock(return_value=True)
locker._submit_manual_workout_form()
locker._mw_handled_by_date.assert_called_once()
locker._on_manual_workout_saved.assert_not_called()