screen-locker/screen_locker/tests/test_sick_panel.py
Krzysztof kuhy Rudnicki 8450644a56 Lock screen: "choose lockers…" opens sickdays' KeepPanel in the lock
Next to the one-tap "I'm sick" (unchanged: keeps nothing, unlocks), a small
secondary link lays sickdays 0.3.0's embeddable KeepPanel over every
surface, in-process so it gets input under the lock's grab. Closing the
overlay reveals the untouched screen underneath, so no countdown or phone
check is restarted or skipped.

After the panel's own write + sync: sick with the workout lock standing
down -> sick floor, decision, close (shared with the one-tap path, no second
mark or push); sick with the workout kept -> sick floor and a note, lock
stays; cleared / unchanged / cancel -> the panel just closes.

Tests stub KeepPanel and redirect resolve_paths and sync_quietly in every
sickdays.* module, sickdays.gui._panel included. The fit check measures the
panel on a throwaway pool. Pins bumped to sickdays-v0.3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
2026-10-17 14:06:22 +02:00

251 lines
8.9 KiB
Python

"""The lock's "choose lockers" link: sickdays' KeepPanel, embedded in the lock.
A recording fake replaces the panel. The pool is written for real before
``on_saved`` fires: the lock branches on the pool, not on the entry.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
import sickdays
import sickdays.gui._panel
from screen_locker import _ui_flows_sick, _ui_flows_sick_panel
from screen_locker._ui_flows_sick_panel import KEPT_NOTE, LINK_TEXT, build_sick_panel
from screen_locker.tests._sick_helpers import mark_sick
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from datetime import date
from pathlib import Path
from screen_locker.screen_lock import ScreenLocker
_MADE: list[FakePanel] = []
class FakePanel:
"""Records how the lock built it; ``on_saved``/``on_cancel`` are driven."""
def __init__(
self,
parent: MagicMock,
day: date,
*,
on_saved: Callable[[sickdays.SickDay | None], None],
on_cancel: Callable[[], None] | None = None,
paths: sickdays.Paths | None = None,
) -> None:
"""Keep every argument for the assertions."""
self.parent = parent
self.day = day
self.on_saved = on_saved
self.on_cancel = on_cancel
self.paths = paths
self.packed = False
_MADE.append(self)
def pack(self, **_kwargs: object) -> None:
"""Record that the lock showed it."""
self.packed = True
def _frame(*args: object, **_kwargs: object) -> MagicMock:
"""A distinct mock frame that remembers its parent, like Tk's ``master``."""
frame = MagicMock()
frame.master = args[0] if args else None
frame.winfo_children.return_value = []
return frame
@pytest.fixture
def panels() -> Iterator[list[FakePanel]]:
"""Every KeepPanel the lock builds, in order."""
_MADE.clear()
with patch.object(_ui_flows_sick_panel, "KeepPanel", FakePanel):
yield _MADE
@pytest.fixture
def locker(
mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> ScreenLocker:
"""A retry-screen lock whose floor and close are observable."""
built = create_locker(mock_tk, tmp_path)
mock_tk.Frame.side_effect = _frame
object.__setattr__(built, "_apply_sick_floor", MagicMock())
object.__setattr__(built, "close", MagicMock())
return built
def _backdrop(panel: FakePanel) -> MagicMock:
"""The full-surface frame the panel's centred box sits on."""
return panel.parent.master
def _texts(mock: MagicMock) -> list[str]:
return [str(c.kwargs.get("text", "")) for c in mock.call_args_list]
class TestLink:
def test_the_link_sits_next_to_the_one_tap_button(
self, mock_tk: MagicMock, locker: ScreenLocker
) -> None:
locker._show_retry_and_sick("nope")
buttons = mock_tk.Button.call_args_list
texts = _texts(mock_tk.Button)
assert "I'm sick" in texts
assert LINK_TEXT in texts
sick = next(c for c in buttons if c.kwargs.get("text") == "I'm sick")
link = next(c for c in buttons if c.kwargs.get("text") == LINK_TEXT)
assert sick.kwargs["command"] == locker.mark_sick_and_unlock
assert link.kwargs["command"] == locker.open_sick_panel
assert texts.index(LINK_TEXT) == texts.index("I'm sick") + 1
def test_opening_builds_todays_panel_on_a_backdrop(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
assert len(panels) == len(locker.container.surfaces)
panel = panels[0]
assert panel.day == sickdays.today()
assert panel.packed
assert panel.on_cancel is not None
_backdrop(panel).place.assert_called_once_with(
relx=0, rely=0, relwidth=1, relheight=1
)
@pytest.mark.usefixtures("dual_output")
def test_every_monitor_gets_a_panel(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
assert len(panels) == 2
def test_reopening_closes_the_previous_panel(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
first = _backdrop(panels[0])
locker.open_sick_panel()
first.destroy.assert_called_once_with()
def test_build_sick_panel_passes_paths_through(
self, mock_tk: MagicMock, panels: list[FakePanel], tmp_path: Path
) -> None:
paths = sickdays.Paths.under(tmp_path / "pool")
saved, cancel = MagicMock(), MagicMock()
build_sick_panel(
MagicMock(), MagicMock(), on_saved=saved, on_cancel=cancel, paths=paths
)
assert panels[0].paths is paths
assert panels[0].on_saved is saved
assert panels[0].on_cancel is cancel
class TestSaved:
@pytest.fixture(autouse=True)
def _no_mark_no_push(self) -> Iterator[tuple[MagicMock, MagicMock]]:
with (
patch.object(_ui_flows_sick, "mark_today_sick") as mark,
patch.object(_ui_flows_sick, "push_in_background") as push,
):
yield mark, push
# The panel already wrote and started its own sync.
mark.assert_not_called()
push.assert_not_called()
def test_sick_and_standing_down_unlocks_like_the_one_tap(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
day = mark_sick(keep=[sickdays.LOCKER_LEETCODE])
entry = sickdays.SickDay(
day=day, is_sick=True, keep=frozenset({sickdays.LOCKER_LEETCODE})
)
with patch("screen_locker._startup_checks.record_decision") as record:
panels[0].on_saved(entry)
locker._apply_sick_floor.assert_called_once_with()
assert record.call_args.args[0].reason == "sick_day_marked"
locker.close.assert_called_once_with()
def test_sick_but_workout_kept_stays_locked_with_a_note(
self, mock_tk: MagicMock, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
backdrop = _backdrop(panels[0])
day = mark_sick(keep=[sickdays.LOCKER_WORKOUT])
entry = sickdays.SickDay(
day=day, is_sick=True, keep=frozenset({sickdays.LOCKER_WORKOUT})
)
panels[0].on_saved(entry)
# The 20:00 floor applies on any sick day, keep or not.
locker._apply_sick_floor.assert_called_once_with()
locker.close.assert_not_called()
backdrop.destroy.assert_called_once_with()
assert KEPT_NOTE in _texts(mock_tk.Label)
back = next(
c for c in mock_tk.Button.call_args_list if c.kwargs.get("text") == "Back"
)
back.kwargs["command"]()
assert locker._sick_overlay is None
def test_cleared_closes_the_panel_and_leaves_the_lock(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
backdrop = _backdrop(panels[0])
entry = sickdays.SickDay(day=sickdays.today(), is_sick=False)
panels[0].on_saved(entry)
backdrop.destroy.assert_called_once_with()
locker._apply_sick_floor.assert_not_called()
locker.close.assert_not_called()
def test_unchanged_save_closes_the_panel_only(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
backdrop = _backdrop(panels[0])
panels[0].on_saved(None)
backdrop.destroy.assert_called_once_with()
locker._apply_sick_floor.assert_not_called()
locker.close.assert_not_called()
def test_cancel_closes_the_panel_only(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
locker.open_sick_panel()
backdrop = _backdrop(panels[0])
assert panels[0].on_cancel is not None
panels[0].on_cancel()
backdrop.destroy.assert_called_once_with()
assert locker._sick_overlay is None
locker._apply_sick_floor.assert_not_called()
locker.close.assert_not_called()
class TestOneTapUnchanged:
def test_one_tap_still_keeps_nothing_and_builds_no_panel(
self, locker: ScreenLocker, panels: list[FakePanel]
) -> None:
with patch.object(_ui_flows_sick, "push_in_background") as push:
locker.mark_sick_and_unlock()
assert sickdays.is_sick_day() is True
assert sickdays.kept_lockers() == frozenset()
push.assert_called_once_with()
locker.close.assert_called_once_with()
assert panels == []
def test_the_pool_fixture_reaches_the_embedded_panel(tmp_path: Path) -> None:
"""``sickdays.gui._panel`` binds its own ``resolve_paths``/``sync_quietly``."""
assert sickdays.gui._panel.resolve_paths(None).log == (
sickdays.Paths.under(tmp_path / "sickdays").log
)
assert sickdays.gui._panel.sync_quietly() is False