mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:07:03 +01:00
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
214 lines
6.3 KiB
Python
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
|