215 lines
9.5 KiB
Python
215 lines
9.5 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") == "before"', delegate_source)
|
|
self.assertIn('marker.get("mode") == "after"', delegate_source)
|
|
self.assertIn("drag_marker_visible", delegate_source)
|
|
self.assertIn("active=is_active 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()
|