mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:03:11 +02:00
Add scripts/check_file_length.py and a max-file-length pre-commit hook that fails any Python/shell file exceeding 400 lines. Extract UIWidgetsMixin and UIFlowsRelaxedMixin from screen_lock.py and _ui_flows.py respectively, and split 6 oversized test files into part2/part3/part4 siblings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
"""Tests for sick justification submission, commitment prompt, and paste disable."""
|
|
# pylint: disable=protected-access
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from screen_locker import _sick_tracker
|
|
from screen_locker._sick_tracker import SickHistory
|
|
from screen_locker.tests.conftest import create_locker
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
class TestSubmitSickJustification:
|
|
"""Tests for _submit_sick_justification validation + persistence."""
|
|
|
|
def _setup_locker(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
tmp_path: Path,
|
|
*,
|
|
fields: dict[str, object] | None = None,
|
|
) -> object:
|
|
defaults: dict[str, object] = {
|
|
"symptom": "fever",
|
|
"onset": "last night",
|
|
"severity": 7,
|
|
"text": "x" * 200,
|
|
}
|
|
if fields:
|
|
defaults.update(fields)
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker._sick_history_cache = SickHistory()
|
|
locker._sick_symptom_var = MagicMock()
|
|
locker._sick_symptom_var.get.return_value = defaults["symptom"]
|
|
locker._sick_onset_var = MagicMock()
|
|
locker._sick_onset_var.get.return_value = defaults["onset"]
|
|
locker._sick_severity_var = MagicMock()
|
|
locker._sick_severity_var.get.return_value = defaults["severity"]
|
|
locker._sick_text_widget = MagicMock()
|
|
locker._sick_text_widget.get.return_value = defaults["text"]
|
|
locker._sick_error_label = MagicMock()
|
|
object.__setattr__(locker, "_proceed_to_sick_countdown", MagicMock())
|
|
return locker
|
|
|
|
def test_validation_failure_displays_error(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = self._setup_locker(mock_tk, tmp_path, fields={"symptom": ""})
|
|
locker._submit_sick_justification()
|
|
locker._sick_error_label.config.assert_called_once()
|
|
locker._proceed_to_sick_countdown.assert_not_called()
|
|
|
|
def test_severity_tcl_error_treated_as_invalid(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = self._setup_locker(mock_tk, tmp_path)
|
|
locker._sick_severity_var.get.side_effect = ValueError("bad")
|
|
locker._submit_sick_justification()
|
|
locker._sick_error_label.config.assert_called_once()
|
|
|
|
def test_save_failure_displays_error(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = self._setup_locker(mock_tk, tmp_path)
|
|
with patch.object(_sick_tracker, "save_history", return_value=False):
|
|
locker._submit_sick_justification()
|
|
locker._sick_error_label.config.assert_called_once()
|
|
locker._proceed_to_sick_countdown.assert_not_called()
|
|
|
|
def test_success_proceeds_to_countdown(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = self._setup_locker(mock_tk, tmp_path)
|
|
with patch.object(_sick_tracker, "save_history", return_value=True):
|
|
locker._submit_sick_justification()
|
|
locker._proceed_to_sick_countdown.assert_called_once()
|
|
|
|
|
|
class TestCommitmentPrompt:
|
|
"""Tests for _show_commitment_prompt + _tick_commitment_timeout + answer."""
|
|
|
|
def test_show_prompt_renders_buttons(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
on_done = MagicMock()
|
|
locker._show_commitment_prompt(on_done=on_done)
|
|
assert locker._commitment_done_fn is on_done
|
|
assert locker._commitment_remaining > 0
|
|
|
|
def test_tick_decrements(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker._commitment_remaining = 2
|
|
locker._commitment_timer_label = MagicMock()
|
|
locker._tick_commitment_timeout()
|
|
assert locker._commitment_remaining == 1
|
|
locker.root.after.assert_called()
|
|
|
|
def test_tick_zero_auto_answers_no(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
on_done = MagicMock()
|
|
locker._commitment_done_fn = on_done
|
|
locker._commitment_remaining = 0
|
|
locker._commitment_timer_label = MagicMock()
|
|
locker._tick_commitment_timeout()
|
|
on_done.assert_called_once()
|
|
|
|
def test_answer_yes_persists_commitment(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
on_done = MagicMock()
|
|
locker._commitment_done_fn = on_done
|
|
history = SickHistory()
|
|
with (
|
|
patch.object(_sick_tracker, "load_history", return_value=history),
|
|
patch.object(_sick_tracker, "save_history", return_value=True) as mock_save,
|
|
):
|
|
locker._answer_commitment(commit=True)
|
|
mock_save.assert_called_once()
|
|
on_done.assert_called_once()
|
|
assert locker._commitment_done_fn is None
|
|
|
|
def test_answer_no_skips_persistence(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
on_done = MagicMock()
|
|
locker._commitment_done_fn = on_done
|
|
with patch.object(_sick_tracker, "save_history") as mock_save:
|
|
locker._answer_commitment(commit=False)
|
|
mock_save.assert_not_called()
|
|
on_done.assert_called_once()
|
|
|
|
def test_answer_with_no_done_fn_is_safe(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
# No _commitment_done_fn attribute set.
|
|
locker._answer_commitment(commit=False)
|
|
|
|
|
|
class TestDisablePaste:
|
|
"""Tests for the _disable_paste helper."""
|
|
|
|
def test_swallows_tcl_error(self) -> None:
|
|
import tkinter as tk
|
|
|
|
from screen_locker._sick_dialog import _disable_paste
|
|
|
|
widget = MagicMock()
|
|
widget.bind.side_effect = tk.TclError("nope")
|
|
# Should not raise.
|
|
_disable_paste(widget)
|