testsAndMisc/python_pkg/wsg_grabber/tests/test_ui_idle.py
Krzysztof kuhy Rudnicki 4b2cd0c398 Clear the wsg_grabber test modules: python_pkg is now under the cap
The last six over-cap files split into eleven new modules, in two passes
since the first cut left five still over. python_pkg has no violations
left at all, down from 50 when this effort started.

Each split carries the whole preamble including the module's pytest
fixtures - started, toolkit and session are taken as parameters and
resolve lexically, so a hand-written import block would have missed
them. ruff then pruned 82 unused imports.

Pass count is 277 before and after, checked after both passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:19:26 +02:00

205 lines
5.5 KiB
Python

"""Tests for the review window when the queue runs dry."""
from __future__ import annotations
import queue
from typing import TYPE_CHECKING, cast
from unittest.mock import MagicMock, patch
import pytest
from python_pkg.wsg_grabber import review, ui
from python_pkg.wsg_grabber.models import (
ReviewCommand,
ReviewedItem,
ReviewItem,
Verdict,
)
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
from python_pkg.wsg_grabber.models import DownloadEvent
from python_pkg.wsg_grabber.review import SessionState
@pytest.fixture
def toolkit() -> Iterator[MagicMock]:
"""Replace tkinter in the ui module's namespace.
Yields:
MagicMock: The stand-in toolkit.
"""
fake = MagicMock()
# a fresh mock per widget, else status and filename are the same object
fake.Frame.side_effect = lambda *_a, **_k: MagicMock()
fake.Label.side_effect = lambda *_a, **_k: MagicMock()
fake.Button.side_effect = lambda *_a, **_k: MagicMock()
fake.BOTH = "both"
fake.X = "x"
fake.LEFT = "left"
fake.RIGHT = "right"
fake.BOTTOM = "bottom"
with patch.object(ui, "tk", fake):
yield fake
def _mock(widget: object) -> MagicMock:
"""View a mocked widget as the MagicMock it really is.
The Widgets dataclass is typed with real Tk classes, so mypy will not let
the tests reach ``call_args`` without this.
Args:
widget: The stand-in widget.
Returns:
MagicMock: The same object, typed for assertions.
"""
return cast("MagicMock", widget)
def _item(tmp_path: Path, name: str = "a") -> ReviewItem:
"""Build a ReviewItem backed by a real file.
Args:
tmp_path: Per-test temporary directory.
name: Distinguishes items.
Returns:
ReviewItem: Fixture value.
"""
path = tmp_path / f"{name}.webm"
path.write_bytes(b"video")
return ReviewItem(
md5=f"{name}-md5",
path=path,
orig_name=f"{name}.webm",
fsize=5,
width=4,
height=3,
)
def _callbacks() -> tuple[ui.Callbacks, MagicMock]:
"""Build callbacks that record what the window asked for.
Returns:
tuple: The callbacks and the recorder behind them.
"""
recorder = MagicMock()
_undone: dict[str, ReviewedItem | None] = {"item": None}
def commit(state: SessionState, choice: Verdict) -> SessionState:
recorder.commit(choice)
if state.current is not None:
_undone["item"] = ReviewedItem(
item=state.current,
choice=choice,
reviewed_name=state.current.path.name,
)
return review.on_verdict(state, choice)
def missing(state: SessionState) -> SessionState:
recorder.missing()
return review.on_missing_locally(state)
def undo(state: SessionState) -> SessionState:
recorder.undo()
action = _undone["item"]
if action is None or not review.can_undo(state):
return state
return review.on_undo(state, action)
return (
ui.Callbacks(
commit=commit,
missing=missing,
undo=undo,
shutdown=recorder.shutdown,
),
recorder,
)
def _window(
state: SessionState,
events: queue.SimpleQueue[DownloadEvent] | None = None,
) -> tuple[ui.ReviewWindow, MagicMock]:
"""Build a window over a mocked toolkit.
Args:
state: Starting state.
events: Optional event queue.
Returns:
tuple: The window and the callback recorder.
"""
callbacks, recorder = _callbacks()
window = ui.ReviewWindow(state, events or queue.SimpleQueue(), callbacks)
return window, recorder
def test_apply_stops_and_disables_when_idle(toolkit: MagicMock) -> None:
window, _ = _window(review.initial_state(0, []))
player = MagicMock()
window.attach_player(player)
window.apply(
ReviewCommand(
play=None,
stop=True,
status="s",
title="t",
filename="f",
verdicts_enabled=False,
undo_enabled=False,
quit_app=False,
),
)
# nothing was playing, so there is nothing to stop; the buttons still go dead
player.stop.assert_not_called()
_mock(window.widgets.keep).configure.assert_called_with(state="disabled")
def test_apply_quits_when_told_to(toolkit: MagicMock) -> None:
window, recorder = _window(review.initial_state(0, []))
window.apply(
ReviewCommand(
play=None,
stop=False,
status="s",
title="t",
filename="f",
verdicts_enabled=False,
undo_enabled=False,
quit_app=True,
),
)
recorder.shutdown.assert_called_once()
def test_playing_without_a_player_is_a_no_op(
toolkit: MagicMock,
tmp_path: Path,
) -> None:
window, _ = _window(review.initial_state(1, [_item(tmp_path)]))
window.refresh()
def test_a_video_that_vanished_is_skipped(
toolkit: MagicMock,
tmp_path: Path,
) -> None:
"""A file can disappear between download and review; don't hang on it."""
first = _item(tmp_path, "a")
state = review.initial_state(2, [first, _item(tmp_path, "b")])
window, recorder = _window(state)
first.path.unlink()
player = MagicMock()
window.attach_player(player)
recorder.missing.assert_called_once()
assert window.state.current is not None
assert window.state.current.md5 == "b-md5"