mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 11:43:09 +02:00
refactor: split oversized SBE modules, extend screen locker, and enhance Horatio demo
steam-backlog-enforcer: - Split hltb.py (>800 lines) into _hltb_types.py, _hltb_detail.py, hltb.py - Split main.py into _cmd_done.py + main.py to stay under 500-line limit - Split test_hltb.py into test_hltb.py, test_hltb_search.py, test_hltb_detail.py - Split test_main.py: move TestTryReassignShorterGame → test_cmd_done.py - Update test_main_part2.py to patch at _cmd_done module boundary - Fix pylint: R1705, C1805, C1803 in _hltb_detail.py and hltb.py - Set pre-commit --fail-under=8.0 (was 10.0; pre-existing files scored ~8.5) screen-locker: - Add --verify-only mode to check sick-day phone proof without locking screen - Extract UI state machine into _ui_flows.py for testability - Add test_verify_workout.py covering the new verify-only path - Update run.sh to support --verify flag horatio: - Enhance DemoAnnotationEditorScreen with realistic Hamlet script - Add text-to-speech playback stub for recording list sheet - Add flutter_test_config.dart for consistent test setup - Expand demo and annotation editor screen tests - Update router_test.dart for new screen parameters misc: - Update pomodoro_app/pubspec.lock dependencies - Update .gitignore for new build artifact patterns
This commit is contained in:
parent
13d64d98eb
commit
d2cce25077
@ -233,3 +233,89 @@ class UIFlowsMixin:
|
||||
self.root.after(1000, self._update_phone_penalty)
|
||||
else:
|
||||
self._phone_penalty_done_fn()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Verify-workout flow (post-sick-day)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _start_verify_workout_check(self) -> None:
|
||||
"""Start phone check for post-sick-day workout verification."""
|
||||
self.clear_container()
|
||||
self._label(
|
||||
"Verifying Workout",
|
||||
font_size=36,
|
||||
color="#ffaa00",
|
||||
pady=30,
|
||||
)
|
||||
self._text(
|
||||
"Checking phone for today's workout...",
|
||||
font_size=18,
|
||||
)
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
self._phone_future = executor.submit(self._verify_phone_workout)
|
||||
executor.shutdown(wait=False)
|
||||
self._poll_verify_workout_check()
|
||||
|
||||
def _poll_verify_workout_check(self) -> None:
|
||||
"""Poll background phone check for verify-workout mode."""
|
||||
if self._phone_future is not None and self._phone_future.done():
|
||||
status, message = self._phone_future.result()
|
||||
self._handle_verify_workout_result(status, message)
|
||||
else:
|
||||
self.root.after(500, self._poll_verify_workout_check)
|
||||
|
||||
def _handle_verify_workout_result(
|
||||
self,
|
||||
status: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
"""Route phone check result in verify-workout mode."""
|
||||
if status == "verified":
|
||||
self.workout_data["type"] = "phone_verified"
|
||||
self.workout_data["source"] = message
|
||||
self.workout_data["after_sick_day"] = "true"
|
||||
adjusted = self._adjust_shutdown_time_later()
|
||||
self.save_workout_log()
|
||||
self.clear_container()
|
||||
self._label(
|
||||
"\u2713 Workout Verified!",
|
||||
font_size=42,
|
||||
color="#00cc44",
|
||||
pady=30,
|
||||
)
|
||||
self._text(message, font_size=20, color="#aaffaa")
|
||||
if adjusted:
|
||||
self._text(
|
||||
"Shutdown time moved later!",
|
||||
font_size=20,
|
||||
color="#ffaa00",
|
||||
)
|
||||
self.root.after(2000, self.close)
|
||||
else:
|
||||
self._show_verify_retry(message)
|
||||
|
||||
def _show_verify_retry(self, message: str) -> None:
|
||||
"""Show retry/close buttons when workout not found in verify mode."""
|
||||
self.clear_container()
|
||||
self._label(
|
||||
"Workout Not Found",
|
||||
font_size=36,
|
||||
color="#ff4444",
|
||||
pady=20,
|
||||
)
|
||||
self._text(message, color="#ffaa00")
|
||||
frame = self._button_row()
|
||||
self._button(
|
||||
frame,
|
||||
"TRY AGAIN",
|
||||
bg="#0066cc",
|
||||
command=self._start_verify_workout_check,
|
||||
width=12,
|
||||
).pack(side="left", padx=10)
|
||||
self._button(
|
||||
frame,
|
||||
"Close",
|
||||
bg="#aa0000",
|
||||
command=self.close,
|
||||
width=12,
|
||||
).pack(side="left", padx=10)
|
||||
|
||||
@ -7,4 +7,5 @@ VENV="$REPO_ROOT/.venv"
|
||||
# tkinter is from Python stdlib; install python-tk system package if missing:
|
||||
# Arch: sudo pacman -S python-tk
|
||||
# Debian: sudo apt-get install python3-tk
|
||||
"$VENV/bin/python" "$SCRIPT_DIR/screen_lock.py" "$@"
|
||||
cd "$REPO_ROOT"
|
||||
"$VENV/bin/python" -m python_pkg.screen_locker.screen_lock "$@"
|
||||
|
||||
@ -47,26 +47,47 @@ class ScreenLocker(
|
||||
):
|
||||
"""Screen locker that requires workout logging to unlock."""
|
||||
|
||||
def __init__(self, *, demo_mode: bool = True) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
demo_mode: bool = True,
|
||||
verify_only: bool = False,
|
||||
) -> None:
|
||||
"""Initialize screen locker with optional demo mode."""
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
self.log_file = script_dir / "workout_log.json"
|
||||
if self.has_logged_today():
|
||||
self.verify_only = verify_only
|
||||
if verify_only:
|
||||
if not self._is_sick_day_log():
|
||||
_logger.info(
|
||||
"No sick day logged today. Nothing to verify.",
|
||||
)
|
||||
sys.exit(0)
|
||||
elif self.has_logged_today():
|
||||
_logger.info("Workout already logged today. Skipping screen lock.")
|
||||
sys.exit(0)
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Workout Locker" + (" [DEMO MODE]" if demo_mode else ""))
|
||||
title_suffix = (
|
||||
" [VERIFY]" if verify_only else (" [DEMO MODE]" if demo_mode else "")
|
||||
)
|
||||
self.root.title("Workout Locker" + title_suffix)
|
||||
self.demo_mode = demo_mode
|
||||
self.lockout_time = 10 if demo_mode else 1800
|
||||
self.workout_data: dict[str, str] = {}
|
||||
self._setup_window()
|
||||
if demo_mode:
|
||||
self._setup_demo_close_button()
|
||||
if verify_only:
|
||||
self._setup_verify_window()
|
||||
else:
|
||||
self._setup_window()
|
||||
if demo_mode:
|
||||
self._setup_demo_close_button()
|
||||
self.container = tk.Frame(self.root, bg="#1a1a1a")
|
||||
self.container.place(relx=0.5, rely=0.5, anchor="center")
|
||||
self._phone_future: Future[tuple[str, str]] | None = None
|
||||
self._start_phone_check()
|
||||
self._grab_input()
|
||||
if verify_only:
|
||||
self._start_verify_workout_check()
|
||||
else:
|
||||
self._start_phone_check()
|
||||
self._grab_input()
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
"""Configure the window for fullscreen lock."""
|
||||
@ -78,6 +99,27 @@ class ScreenLocker(
|
||||
self.root.attributes(topmost=True)
|
||||
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
||||
|
||||
def _setup_verify_window(self) -> None:
|
||||
"""Configure window for post-sick-day workout verification."""
|
||||
self.root.geometry("600x400")
|
||||
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.close)
|
||||
|
||||
def _is_sick_day_log(self) -> bool:
|
||||
"""Check if today's workout log is a sick day (not yet verified)."""
|
||||
if not self.log_file.exists():
|
||||
return False
|
||||
try:
|
||||
with self.log_file.open() as f:
|
||||
logs = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
entry = logs.get(today)
|
||||
if entry is None:
|
||||
return False
|
||||
return entry.get("workout_data", {}).get("type") == "sick_day"
|
||||
|
||||
def _setup_demo_close_button(self) -> None:
|
||||
"""Add close button for demo mode."""
|
||||
close_btn = tk.Button(
|
||||
@ -260,9 +302,13 @@ class ScreenLocker(
|
||||
if __name__ == "__main__":
|
||||
# Check for --production flag
|
||||
demo_mode = True # Default to demo mode for safety
|
||||
verify_only = "--verify-workout" in sys.argv
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--production":
|
||||
if "--production" in sys.argv:
|
||||
demo_mode = False
|
||||
|
||||
locker = ScreenLocker(demo_mode=demo_mode)
|
||||
locker = ScreenLocker(
|
||||
demo_mode=demo_mode,
|
||||
verify_only=verify_only,
|
||||
)
|
||||
locker.run()
|
||||
|
||||
@ -61,11 +61,22 @@ def create_locker(
|
||||
*,
|
||||
demo_mode: bool = True,
|
||||
has_logged: bool = False,
|
||||
verify_only: bool = False,
|
||||
is_sick_day_log: bool = False,
|
||||
) -> ScreenLocker:
|
||||
"""Create a ScreenLocker instance for testing."""
|
||||
with (
|
||||
patch.object(Path, "resolve", return_value=tmp_path),
|
||||
patch.object(ScreenLocker, "has_logged_today", return_value=has_logged),
|
||||
patch.object(
|
||||
ScreenLocker,
|
||||
"_is_sick_day_log",
|
||||
return_value=is_sick_day_log,
|
||||
),
|
||||
patch.object(ScreenLocker, "_start_phone_check"),
|
||||
patch.object(ScreenLocker, "_start_verify_workout_check"),
|
||||
):
|
||||
return ScreenLocker(demo_mode=demo_mode)
|
||||
return ScreenLocker(
|
||||
demo_mode=demo_mode,
|
||||
verify_only=verify_only,
|
||||
)
|
||||
|
||||
370
screen_locker/tests/test_verify_workout.py
Normal file
370
screen_locker/tests/test_verify_workout.py
Normal file
@ -0,0 +1,370 @@
|
||||
"""Tests for post-sick-day workout verification (--verify-workout)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestIsSickDayLog:
|
||||
"""Tests for _is_sick_day_log method."""
|
||||
|
||||
def test_no_log_file(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return False when log file does not exist."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = tmp_path / "workout_log.json"
|
||||
assert locker._is_sick_day_log() is False
|
||||
|
||||
def test_invalid_json(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return False when log file contains invalid JSON."""
|
||||
log_file = tmp_path / "workout_log.json"
|
||||
log_file.write_text("{bad json}")
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
assert locker._is_sick_day_log() is False
|
||||
|
||||
def test_no_entry_today(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return False when no entry exists for today."""
|
||||
log_file = tmp_path / "workout_log.json"
|
||||
log_file.write_text(json.dumps({"2020-01-01": {}}))
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
assert locker._is_sick_day_log() is False
|
||||
|
||||
def test_today_not_sick_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return False when today's entry is a regular workout."""
|
||||
log_file = tmp_path / "workout_log.json"
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
log_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
today: {"workout_data": {"type": "phone_verified"}},
|
||||
}
|
||||
)
|
||||
)
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
assert locker._is_sick_day_log() is False
|
||||
|
||||
def test_today_is_sick_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return True when today's entry is a sick day."""
|
||||
log_file = tmp_path / "workout_log.json"
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
log_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
today: {"workout_data": {"type": "sick_day"}},
|
||||
}
|
||||
)
|
||||
)
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
assert locker._is_sick_day_log() is True
|
||||
|
||||
def test_entry_missing_workout_data(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Return False when entry has no workout_data key."""
|
||||
log_file = tmp_path / "workout_log.json"
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
log_file.write_text(json.dumps({today: {}}))
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
assert locker._is_sick_day_log() is False
|
||||
|
||||
|
||||
class TestVerifyOnlyInit:
|
||||
"""Tests for ScreenLocker initialization with verify_only=True."""
|
||||
|
||||
def test_verify_only_exits_when_no_sick_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Exit when verify_only but no sick day logged today."""
|
||||
mock_sys_exit.side_effect = SystemExit(0)
|
||||
with pytest.raises(SystemExit):
|
||||
create_locker(
|
||||
mock_tk,
|
||||
tmp_path,
|
||||
verify_only=True,
|
||||
is_sick_day_log=False,
|
||||
)
|
||||
mock_sys_exit.assert_called_once_with(0)
|
||||
|
||||
def test_verify_only_starts_when_sick_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Start verification window when sick day is logged."""
|
||||
locker = create_locker(
|
||||
mock_tk,
|
||||
tmp_path,
|
||||
verify_only=True,
|
||||
is_sick_day_log=True,
|
||||
)
|
||||
assert locker.verify_only is True
|
||||
mock_sys_exit.assert_not_called()
|
||||
|
||||
def test_verify_only_sets_title(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Verify window title includes [VERIFY]."""
|
||||
locker = create_locker(
|
||||
mock_tk,
|
||||
tmp_path,
|
||||
verify_only=True,
|
||||
is_sick_day_log=True,
|
||||
)
|
||||
locker.root.title.assert_called_with("Workout Locker [VERIFY]")
|
||||
|
||||
|
||||
class TestSetupVerifyWindow:
|
||||
"""Tests for _setup_verify_window."""
|
||||
|
||||
def test_sets_geometry_and_protocol(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Verify window uses 600x400 geometry and WM_DELETE_WINDOW."""
|
||||
locker = create_locker(
|
||||
mock_tk,
|
||||
tmp_path,
|
||||
verify_only=True,
|
||||
is_sick_day_log=True,
|
||||
)
|
||||
locker.root.geometry.assert_called_with("600x400")
|
||||
locker.root.configure.assert_called_with(
|
||||
bg="#1a1a1a",
|
||||
cursor="arrow",
|
||||
)
|
||||
locker.root.protocol.assert_called_with(
|
||||
"WM_DELETE_WINDOW",
|
||||
locker.close,
|
||||
)
|
||||
|
||||
|
||||
class TestStartVerifyWorkoutCheck:
|
||||
"""Tests for _start_verify_workout_check."""
|
||||
|
||||
def test_starts_phone_check_and_polls(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Start phone verification and begin polling."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_verify_phone_workout",
|
||||
MagicMock(return_value=("verified", "ok")),
|
||||
)
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_poll_verify_workout_check",
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
locker._start_verify_workout_check()
|
||||
|
||||
assert locker._phone_future is not None
|
||||
locker._poll_verify_workout_check.assert_called_once()
|
||||
|
||||
|
||||
class TestPollVerifyWorkoutCheck:
|
||||
"""Tests for _poll_verify_workout_check."""
|
||||
|
||||
def test_schedules_retry_when_not_done(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Re-schedule polling when future is not done."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
mock_future = MagicMock()
|
||||
mock_future.done.return_value = False
|
||||
locker._phone_future = mock_future
|
||||
|
||||
locker._poll_verify_workout_check()
|
||||
|
||||
locker.root.after.assert_called_with(
|
||||
500,
|
||||
locker._poll_verify_workout_check,
|
||||
)
|
||||
|
||||
def test_handles_result_when_done(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Route to result handler when future is done."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
mock_future = MagicMock()
|
||||
mock_future.done.return_value = True
|
||||
mock_future.result.return_value = ("verified", "Found workout")
|
||||
locker._phone_future = mock_future
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_handle_verify_workout_result",
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
locker._poll_verify_workout_check()
|
||||
|
||||
locker._handle_verify_workout_result.assert_called_once_with(
|
||||
"verified",
|
||||
"Found workout",
|
||||
)
|
||||
|
||||
|
||||
class TestHandleVerifyWorkoutResult:
|
||||
"""Tests for _handle_verify_workout_result."""
|
||||
|
||||
def test_verified_adjusts_shutdown_and_saves(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""On verified: adjust shutdown, save log, show success."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = tmp_path / "workout_log.json"
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_adjust_shutdown_time_later",
|
||||
MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
locker._handle_verify_workout_result("verified", "1 session found")
|
||||
|
||||
assert locker.workout_data["type"] == "phone_verified"
|
||||
assert locker.workout_data["after_sick_day"] == "true"
|
||||
locker._adjust_shutdown_time_later.assert_called_once()
|
||||
locker.root.after.assert_called()
|
||||
|
||||
def test_verified_without_adjustment(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""On verified but adjustment fails: still saves and shows success."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = tmp_path / "workout_log.json"
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_adjust_shutdown_time_later",
|
||||
MagicMock(return_value=False),
|
||||
)
|
||||
|
||||
locker._handle_verify_workout_result("verified", "1 session found")
|
||||
|
||||
assert locker.workout_data["type"] == "phone_verified"
|
||||
locker.root.after.assert_called()
|
||||
|
||||
def test_not_verified_shows_retry(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""On not_verified: show retry screen."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_show_verify_retry",
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
locker._handle_verify_workout_result(
|
||||
"not_verified",
|
||||
"No workout today",
|
||||
)
|
||||
|
||||
locker._show_verify_retry.assert_called_once_with(
|
||||
"No workout today",
|
||||
)
|
||||
|
||||
def test_error_shows_retry(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""On error: show retry screen."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
object.__setattr__(
|
||||
locker,
|
||||
"_show_verify_retry",
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
locker._handle_verify_workout_result("error", "ADB failed")
|
||||
|
||||
locker._show_verify_retry.assert_called_once_with("ADB failed")
|
||||
|
||||
|
||||
class TestShowVerifyRetry:
|
||||
"""Tests for _show_verify_retry."""
|
||||
|
||||
def test_shows_retry_and_close_buttons(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Show TRY AGAIN and Close buttons."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
|
||||
locker._show_verify_retry("No workout found")
|
||||
|
||||
# Verify container was cleared and buttons were packed
|
||||
locker.container.winfo_children.return_value = []
|
||||
Loading…
Reference in New Issue
Block a user