testsAndMisc/python_pkg/wsg_grabber/tests/test_ui.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

214 lines
6.3 KiB
Python

"""Tests for the Tk window.
``tkinter`` is replaced wholesale in the module's namespace, so every widget is
a mock and no real ``Tk()`` is ever constructed. That matters beyond speed: CI
has no display, and instantiating Tk there would fail outright.
There is deliberately little to test here, because the window contains no
decisions -- those all live in ``review`` and are covered by ``test_review``.
"""
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.constants import (
KEEP_KEYS,
PASS_KEYS,
QUIT_KEYS,
UNDO_KEYS,
)
from python_pkg.wsg_grabber.models import (
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_the_window_has_a_video_area_and_a_control_bar(toolkit: MagicMock) -> None:
window, _ = _window(review.initial_state(0, []))
assert toolkit.Tk.called
assert toolkit.Button.call_count == 3 # keep, pass, undo
# two frames: the video area and the control bar below it
assert toolkit.Frame.call_count == 2
# the video area expands; the bar sits under it rather than over it, because
# mpv's --wid child window covers whatever shares its frame
assert _mock(window.widgets.video).pack.call_args.kwargs["expand"]
def test_every_shortcut_is_bound(toolkit: MagicMock) -> None:
window, _ = _window(review.initial_state(0, []))
calls = _mock(window.widgets.root).bind.call_args_list
bound = {call.args[0] for call in calls}
assert set(KEEP_KEYS) <= bound
assert set(PASS_KEYS) <= bound
assert set(QUIT_KEYS) <= bound
assert set(UNDO_KEYS) <= bound
def test_keep_and_pass_shortcuts_reach_the_callbacks(
toolkit: MagicMock,
tmp_path: Path,
) -> None:
state = review.initial_state(2, [_item(tmp_path, "a"), _item(tmp_path, "b")])
window, recorder = _window(state)
window.attach_player(MagicMock())
bound = _mock(window.widgets.root).bind.call_args_list
handlers = {call.args[0]: call.args[1] for call in bound}
handlers[KEEP_KEYS[0]](object())
first: object = recorder.commit.call_args.args[0]
assert first is Verdict.KEEP
handlers[PASS_KEYS[0]](object())
second: object = recorder.commit.call_args.args[0]
assert second is Verdict.SKIP
def test_the_quit_shortcut_shuts_down(toolkit: MagicMock) -> None:
window, recorder = _window(review.initial_state(0, []))
bound = _mock(window.widgets.root).bind.call_args_list
handlers = {call.args[0]: call.args[1] for call in bound}
handlers[QUIT_KEYS[0]](object())
recorder.shutdown.assert_called_once()
_mock(window.widgets.root).destroy.assert_called_once()
def test_the_buttons_call_the_same_handlers(
toolkit: MagicMock,
tmp_path: Path,
) -> None:
state = review.initial_state(1, [_item(tmp_path)])
window, recorder = _window(state)
window.attach_player(MagicMock())
window.on_keep()
assert recorder.commit.call_args.args[0] is Verdict.KEEP