zapretgui/tests/test_preset_drag_indicator.py
loop-uh 34c3bf9a77
All checks were successful
Zapret source guards / validate (push) Successful in 13s
Windows-сборка / Dev Dev 21.1.7.200 опубликован
Preset list: highlight the clicked tile at once
A single click waits for the system double-click time (a double click opens the
preset text instead of activating it), and for that half second nothing on the
tile changed, so the click looked ignored. The clicked tile now gets the accent
fill of an active tile right away (no check mark, so it reads as 'chosen, not
active yet'). When the activation fires, the marker slides to a tile that is
already filled instead of lighting it up from nothing. The highlight is dropped
by a double click, shift click, a button click or another click, and by a timer
if the activation fails. The wait itself is unchanged.

Tests compare the rendered tile pixels (before / after click / double click /
shift click / two quick clicks / after activation).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:41:30 -04:00

214 lines
9.4 KiB
Python

from __future__ import annotations
import inspect
import os
import unittest
from unittest.mock import Mock
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QPoint
from PyQt6.QtWidgets import QApplication
from ui.presets_menu import delegate as preset_delegate
from ui.presets_menu import common as preset_common
from ui.presets_menu.model import PresetListModel
from ui.presets_menu import view as preset_view
from presets.ui.common.user_presets_page import UserPresetsPageBase
class PresetDragIndicatorTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_drop_marker_maps_targets_to_clear_visual_modes(self) -> None:
self.assertEqual(
preset_view.preset_drop_marker_for_target(2, "folder"),
{"row": 2, "mode": "folder"},
)
self.assertEqual(
preset_view.preset_drop_marker_for_target(4, "preset"),
{"row": 4, "mode": "before"},
)
self.assertEqual(
preset_view.preset_drop_marker_for_target(-1, "empty"),
{"row": -1, "mode": ""},
)
def test_drop_target_uses_lower_half_as_after_row(self) -> None:
self.assertEqual(
preset_view.preset_drop_target_for_position(4, "preset", y=109, row_top=100, row_height=20),
{"marker": {"row": 4, "mode": "before"}, "destination_kind": "preset", "destination_row": 4},
)
self.assertEqual(
preset_view.preset_drop_target_for_position(4, "preset", y=112, row_top=100, row_height=20),
{"marker": {"row": 4, "mode": "after"}, "destination_kind": "preset_after", "destination_row": 4},
)
def test_adjacent_preset_gap_has_one_canonical_drop_target(self) -> None:
lower_half_target = preset_view.preset_drop_target_for_position(
4,
"preset",
y=112,
row_top=100,
row_height=20,
)
self.assertEqual(
preset_common.preset_canonical_drop_target_for_next_row(
lower_half_target,
next_row=5,
next_kind="preset",
),
{"marker": {"row": 5, "mode": "before"}, "destination_kind": "preset", "destination_row": 5},
)
def test_view_clears_drop_marker_when_drag_finishes_or_leaves(self) -> None:
view_source = inspect.getsource(preset_view.LinkedWheelListView)
self.assertIn("set_drop_marker", view_source)
self.assertIn("dragLeaveEvent", view_source)
self.assertIn("self.set_drop_marker(-1, \"\")", view_source)
def test_internal_drag_does_not_depend_on_windows_ole_drop_target(self) -> None:
view_source = inspect.getsource(preset_view.LinkedWheelListView.mouseMoveEvent)
finish_source = inspect.getsource(preset_view.LinkedWheelListView._finish_internal_drag)
self.assertIn("self._drag_source = (kind, source_id)", view_source)
self.assertIn("self._update_internal_drag", view_source)
self.assertNotIn("QDrag", view_source)
self.assertNotIn("restore_windows_qt_file_drop", view_source)
self.assertIn("self.item_dropped.emit", finish_source)
def test_internal_mouse_drag_emits_the_resolved_preset_move(self) -> None:
view = preset_view.LinkedWheelListView()
self.addCleanup(view.deleteLater)
view.resize(500, 300)
view._drag_source = ("preset", "A.txt")
view._drop_target_at = Mock(
return_value=(
{"marker": {"row": 2, "mode": "before"}, "destination_kind": "preset"},
"B.txt",
"games",
)
)
dropped: list[tuple[str, str, str, str, str]] = []
view.item_dropped.connect(lambda *args: dropped.append(tuple(args)))
self.assertTrue(view._finish_internal_drag(QPoint(20, 20)))
self.assertEqual(dropped, [("preset", "A.txt", "preset", "B.txt", "games")])
self.assertIsNone(view._drag_source)
def test_view_updates_only_drop_marker_rows(self) -> None:
payload_source = inspect.getsource(preset_view.LinkedWheelListView.set_drop_marker_payload)
update_source = inspect.getsource(preset_view.LinkedWheelListView._update_drop_marker_rows)
self.assertIn("_update_drop_marker_rows", payload_source)
self.assertNotIn("viewport().update()", payload_source)
self.assertIn("viewport().update(rect", update_source)
def test_preset_current_index_helper_skips_already_selected_index(self) -> None:
class _Index:
def __init__(self, row: int) -> None:
self.row = row
def __eq__(self, other) -> bool:
return isinstance(other, _Index) and self.row == other.row
index = _Index(3)
view = Mock()
view.currentIndex.return_value = index
self.assertFalse(preset_common.set_current_index_if_changed(view, index))
view.setCurrentIndex.assert_not_called()
def test_view_sends_destination_folder_with_drop(self) -> None:
view_source = inspect.getsource(preset_view.LinkedWheelListView)
self.assertIn("item_dropped = pyqtSignal(str, str, str, str, str)", view_source)
self.assertIn("PresetListModel.FolderKeyRole", view_source)
self.assertIn("destination_folder_key", view_source)
self.assertIn(
"self.item_dropped.emit(source_kind, source_id, destination_kind, destination_id, destination_folder_key)",
view_source,
)
def test_delegate_draws_folder_and_before_row_drop_markers(self) -> None:
delegate_source = inspect.getsource(preset_delegate.PresetListDelegate)
self.assertIn("_paint_drop_marker", delegate_source)
self.assertIn('marker.get("mode") == "folder"', delegate_source)
self.assertIn('marker.get("mode") in ("before", "after")', delegate_source)
self.assertIn("drag_marker_visible", delegate_source)
self.assertIn("active=(is_active or clicked) and not drag_marker_visible", delegate_source)
self.assertIn("show_active_marker=False", delegate_source)
def test_delegate_action_updates_only_affected_preset_row(self) -> None:
click_source = inspect.getsource(preset_delegate.PresetListDelegate._handle_action_click)
clear_source = inspect.getsource(preset_delegate.PresetListDelegate._clear_pending_destructive)
shake_source = inspect.getsource(preset_delegate.PresetListDelegate._advance_pending_shake)
self.assertIn("_update_preset_row_view", click_source)
self.assertIn("_update_pending_destructive_row", clear_source)
self.assertIn("_update_pending_destructive_row", shake_source)
self.assertNotIn("viewport().update()", click_source)
self.assertNotIn("viewport().update()", clear_source)
self.assertNotIn("viewport().update()", shake_source)
def test_model_moves_preset_row_without_full_reload(self) -> None:
model = PresetListModel()
model.set_rows(
[
{"kind": "folder", "folder_key": "common", "name": "Общие", "count": 1, "is_collapsed": False},
{"kind": "preset", "file_name": "A.txt", "name": "A", "folder_key": "common"},
{"kind": "folder", "folder_key": "game-filter", "name": "Game filter", "count": 1, "is_collapsed": False},
{"kind": "preset", "file_name": "B.txt", "name": "B", "folder_key": "game-filter"},
]
)
model.beginResetModel = Mock(side_effect=AssertionError("move must not reset the whole preset list"))
self.assertTrue(model.move_preset("A.txt", "preset_after", "B.txt", "game-filter"))
rows = [
(
model.index(row, 0).data(PresetListModel.KindRole),
model.index(row, 0).data(PresetListModel.FolderKeyRole),
model.index(row, 0).data(PresetListModel.FileNameRole),
model.index(row, 0).data(PresetListModel.CountRole),
)
for row in range(model.rowCount())
]
self.assertIn(("preset", "game-filter", "A.txt", 0), rows)
self.assertNotIn(("preset", "common", "A.txt", 0), rows)
self.assertIn(("folder", "common", "", 0), rows)
self.assertIn(("folder", "game-filter", "", 2), rows)
def test_model_keeps_folder_count_when_preset_moves_inside_same_folder(self) -> None:
model = PresetListModel()
model.set_rows(
[
{"kind": "folder", "folder_key": "common", "name": "Общие", "count": 2, "is_collapsed": False},
{"kind": "preset", "file_name": "A.txt", "name": "A", "folder_key": "common"},
{"kind": "preset", "file_name": "B.txt", "name": "B", "folder_key": "common"},
]
)
model.beginResetModel = Mock(side_effect=AssertionError("move must not reset the whole preset list"))
self.assertTrue(model.move_preset("A.txt", "preset_after", "B.txt", "common"))
self.assertEqual(model.index(0, 0).data(PresetListModel.CountRole), 2)
def test_local_preset_move_preserves_scroll_position(self) -> None:
move_source = inspect.getsource(UserPresetsPageBase._apply_preset_move_locally)
self.assertIn("capture_presets_view_state", move_source)
self.assertIn("restore_presets_view_state", move_source)
self.assertNotIn("ensure_preset_list_current_index", move_source)
if __name__ == "__main__":
unittest.main()