mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 18:53:55 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
5.8 KiB
Python
147 lines
5.8 KiB
Python
"""The lock screens' glyphs (textgate): what each state paints, and what it logs.
|
|
|
|
Every sentence these screens used to paint now goes to the log at WARNING,
|
|
so each test checks both halves: the glyph on screen, the reason in the log.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from screen_locker import _glyphs as g
|
|
from screen_locker import _ui_flows
|
|
from screen_locker._source_health import SourceFinding
|
|
from screen_locker.tests.conftest import create_locker
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _labels(mock_tk: MagicMock) -> list[tuple[str, str]]:
|
|
"""(text, fg) of every Label, in call order."""
|
|
return [
|
|
(c.kwargs.get("text", ""), c.kwargs.get("fg", ""))
|
|
for c in mock_tk.Label.call_args_list
|
|
]
|
|
|
|
|
|
def _warned(caplog: pytest.LogCaptureFixture) -> str:
|
|
"""Every WARNING message of the test, joined."""
|
|
return "\n".join(r.getMessage() for r in caplog.records if r.levelname == "WARNING")
|
|
|
|
|
|
class TestRetryScreen:
|
|
"""``_show_retry_and_sick``: barbell-fail, status glyph, sync faults."""
|
|
|
|
def test_too_short_paints_its_glyph_and_logs_the_reason(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A too-short session shows the short-clock glyph; its reason is logged."""
|
|
monkeypatch.setattr(_ui_flows, "collect_source_findings", list)
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker._handle_startup_phone_result("too_short", "only 12 min")
|
|
labels = _labels(mock_tk)
|
|
palette = locker._colors.palette
|
|
assert (f"{g.LIFT} {g.FAIL}", palette.danger) in labels
|
|
assert (g.SHORT, palette.warning) in labels
|
|
assert "only 12 min" in _warned(caplog)
|
|
assert not any("only 12 min" in text for text, _fg in labels)
|
|
|
|
def test_no_status_paints_no_status_glyph(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Called without a status (the penalty timer's end), no status glyph."""
|
|
monkeypatch.setattr(_ui_flows, "collect_source_findings", list)
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker._show_retry_and_sick("phone gone")
|
|
texts = [text for text, _fg in _labels(mock_tk)]
|
|
assert not any(text in g.STATUS_GLYPH.values() for text in texts)
|
|
assert g.FAIL not in texts
|
|
|
|
def test_each_sync_finding_is_one_glyph_and_logged(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Two unreadable sources: two sync-fail glyphs; the prose goes to the log."""
|
|
findings = [
|
|
SourceFinding("firebase", "firebase unreadable", "401"),
|
|
SourceFinding("mirror", "mirror stale", "9 days"),
|
|
]
|
|
monkeypatch.setattr(_ui_flows, "collect_source_findings", lambda: findings)
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
locker._show_retry_and_sick("x", status="stale")
|
|
assert (
|
|
f"{g.SYNC}{g.FAIL} " * 2,
|
|
locker._colors.palette.warning,
|
|
) in _labels(mock_tk)
|
|
assert "firebase unreadable" in _warned(caplog)
|
|
assert "mirror stale" in _warned(caplog)
|
|
|
|
|
|
class TestAcceptedScreen:
|
|
"""``_paint_accepted``: source glyph + tick, wait dots, then unlock."""
|
|
|
|
def test_verified_lift_paints_tick_and_schedules_unlock(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""A verified lift paints barbell + tick in success, then the wait dots."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(locker.root, "after", MagicMock())
|
|
locker._handle_startup_phone_result("verified", "Workout A")
|
|
labels = _labels(mock_tk)
|
|
assert (f"{g.LIFT} {g.OK}", locker._colors.palette.success) in labels
|
|
assert g.WAIT in [text for text, _fg in labels]
|
|
assert not any("Workout A" in text for text, _fg in labels)
|
|
assert locker.workout_data["source"] == "Workout A"
|
|
delay, callback = locker.root.after.call_args.args
|
|
assert delay > 0
|
|
assert callback.__name__ == "unlock_screen"
|
|
|
|
def test_manual_save_paints_the_form_glyph(
|
|
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
|
) -> None:
|
|
"""A saved manual entry paints form + tick, never the entry's source."""
|
|
locker = create_locker(mock_tk, tmp_path)
|
|
object.__setattr__(locker.root, "after", MagicMock())
|
|
with patch.object(locker, "unlock_screen"):
|
|
locker._on_manual_workout_saved({"source": "table tennis at Solec"})
|
|
texts = [text for text, _fg in _labels(mock_tk)]
|
|
assert f"{g.FORM} {g.OK}" in texts
|
|
assert not any("Solec" in t for t in texts)
|
|
|
|
|
|
class TestGlyphHelpers:
|
|
"""The pure helpers in ``_glyphs``."""
|
|
|
|
def test_dots_fills_up_to_total_and_keeps_the_real_count(self) -> None:
|
|
"""Dots never exceed the total; the numbers stay exact."""
|
|
assert g.dots(2, 4) == "●●○○ 2/4"
|
|
assert g.dots(6, 4) == "●●●● 6/4"
|
|
assert g.dots(-1, 3) == "○○○ -1/3"
|
|
|
|
def test_status_glyph_falls_back_to_fail(self) -> None:
|
|
"""A status with no glyph shows the plain fail mark."""
|
|
assert g.status_glyph("too_short") == g.SHORT
|
|
assert g.status_glyph("brand_new_status") == g.FAIL
|
|
|
|
def test_entry_glyphs_maps_known_and_unknown_types(self) -> None:
|
|
"""Known types get their glyph; an unknown one shows a tick, in order."""
|
|
assert g.entry_glyphs(("runnerup_verified", "mystery")) == f"{g.RUN} {g.OK}"
|
|
assert not g.entry_glyphs(())
|