zapretgui/tests/test_window_preset_file_drop.py

519 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QEvent, QUrl
from PyQt6.QtWidgets import QApplication
class _DropEvent:
def __init__(self, event_type, urls) -> None:
self._event_type = event_type
self._mime_data = SimpleNamespace(
hasUrls=lambda: True,
urls=lambda: list(urls),
)
self.accepted = False
def type(self):
return self._event_type
def mimeData(self): # noqa: N802
return self._mime_data
def acceptProposedAction(self) -> None: # noqa: N802
self.accepted = True
class _Receiver:
def __init__(self, window) -> None:
self._window = window
def window(self):
return self._window
class WindowPresetFileDropTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_main_window_installs_filter_and_routes_to_current_page(self) -> None:
from ui.fluent_app_window import ZapretFluentWindow
from ui.window_preset_file_drop import (
PresetFileDropOverlay,
WindowPresetFileDropFilter,
)
with patch(
"ui.fluent_app_window.enable_windows_file_drop",
return_value=True,
) as enable_native_drop:
window = ZapretFluentWindow()
self.addCleanup(window.deleteLater)
event_filter = window._preset_file_drop_filter
self.addCleanup(self._app.removeEventFilter, event_filter)
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
window.ui_session = SimpleNamespace(
page_host=SimpleNamespace(current_page=lambda: target),
)
self.assertTrue(window.acceptDrops())
self.assertIsInstance(event_filter, WindowPresetFileDropFilter)
self.assertIsInstance(event_filter.overlay, PresetFileDropOverlay)
self.assertTrue(event_filter.overlay.isHidden())
self.assertIs(window._current_preset_file_drop_target(), target)
self.assertTrue(window._windows_file_drop_enabled)
enable_native_drop.assert_called_once_with(window)
def test_windows_window_disables_qt_ole_drop_owner(self) -> None:
from ui.fluent_app_window import ZapretFluentWindow
installed_app_filters: list[object] = []
real_install = QApplication.installEventFilter
def _record_install(app, event_filter):
installed_app_filters.append(event_filter)
return real_install(app, event_filter)
with (
patch("ui.fluent_app_window.use_qt_file_drop", return_value=False),
patch("ui.fluent_app_window.enable_windows_file_drop", return_value=True),
patch.object(QApplication, "installEventFilter", _record_install),
):
window = ZapretFluentWindow()
self.addCleanup(window.deleteLater)
event_filter = window._preset_file_drop_filter
self.addCleanup(self._app.removeEventFilter, event_filter)
self.assertFalse(window.acceptDrops())
self.assertTrue(window._windows_file_drop_enabled)
# Drag-событий Qt на Windows нет: фильтр на всё приложение не ставится,
# а подсказка не создаётся вместе с окном при старте.
self.assertNotIn(event_filter, installed_app_filters)
self.assertIsNone(event_filter._overlay)
def test_rebinds_native_drop_after_qt_recreates_window_handle(self) -> None:
from ui.fluent_app_window import ZapretFluentWindow
with patch(
"ui.fluent_app_window.enable_windows_file_drop",
return_value=True,
) as enable_native_drop:
window = ZapretFluentWindow()
self.addCleanup(window.deleteLater)
event_filter = window._preset_file_drop_filter
self.addCleanup(self._app.removeEventFilter, event_filter)
enable_native_drop.reset_mock()
window.event(QEvent(QEvent.Type.WinIdChange))
enable_native_drop.assert_called_once_with(window)
def test_collects_only_unique_existing_txt_files(self) -> None:
from ui.window_preset_file_drop import dropped_preset_file_paths
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
first = root / "First.txt"
second = root / "SECOND.TXT"
ignored = root / "notes.log"
first.write_text("first", encoding="utf-8")
second.write_text("second", encoding="utf-8")
ignored.write_text("ignored", encoding="utf-8")
mime_data = SimpleNamespace(
hasUrls=lambda: True,
urls=lambda: [
QUrl.fromLocalFile(str(first)),
QUrl.fromLocalFile(str(ignored)),
QUrl.fromLocalFile(str(second)),
QUrl.fromLocalFile(str(first)),
QUrl("https://example.com/remote.txt"),
],
)
self.assertEqual(
dropped_preset_file_paths(mime_data),
[str(first), str(second)],
)
def test_filter_accepts_drag_and_imports_drop_anywhere_in_own_window(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Dragged.txt"
preset_path.write_text("preset", encoding="utf-8")
url = QUrl.fromLocalFile(str(preset_path))
window = object()
receiver = _Receiver(window)
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
overlay = Mock()
event_filter = WindowPresetFileDropFilter(
window,
target_resolver=lambda: target,
overlay=overlay,
)
drag_event = _DropEvent(QEvent.Type.DragEnter, [url])
self.assertTrue(event_filter.eventFilter(receiver, drag_event))
self.assertTrue(drag_event.accepted)
target.import_dropped_preset_files.assert_not_called()
overlay.show_hint.assert_called_once_with([str(preset_path)])
drop_event = _DropEvent(QEvent.Type.Drop, [url])
self.assertTrue(event_filter.eventFilter(receiver, drop_event))
self.assertTrue(drop_event.accepted)
target.import_dropped_preset_files.assert_called_once_with([str(preset_path)])
overlay.show_accepted.assert_called_once_with([str(preset_path)])
overlay.hide_hint.assert_not_called()
leave_event = _DropEvent(QEvent.Type.DragLeave, [])
self.assertFalse(event_filter.eventFilter(receiver, leave_event))
self.assertEqual(overlay.hide_hint.call_count, 1)
def test_native_drop_uses_the_same_filter_import_path(self) -> None:
from ui.window_preset_file_drop import (
WindowPresetFileDropFilter,
handle_native_preset_file_drop,
)
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Native.txt"
preset_path.write_text("preset", encoding="utf-8")
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
event_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: target,
overlay=Mock(),
)
window = SimpleNamespace(_preset_file_drop_filter=event_filter)
with patch(
"ui.window_preset_file_drop.windows_dropped_file_paths",
return_value=[str(preset_path)],
):
handled = handle_native_preset_file_drop(window, object())
self.assertTrue(handled)
target.import_dropped_preset_files.assert_called_once_with([str(preset_path)])
event_filter.overlay.show_accepted.assert_called_once_with([str(preset_path)])
event_filter.overlay.hide_hint.assert_not_called()
def test_native_drop_message_is_consumed_even_without_valid_txt(self) -> None:
from ui.window_preset_file_drop import handle_native_preset_file_drop
event_filter = Mock()
window = SimpleNamespace(_preset_file_drop_filter=event_filter)
with patch(
"ui.window_preset_file_drop.windows_dropped_file_paths",
return_value=["C:/Temp/ignored.log"],
):
handled = handle_native_preset_file_drop(window, object())
self.assertTrue(handled)
event_filter.import_file_paths.assert_called_once_with(["C:/Temp/ignored.log"])
def test_non_drop_native_message_is_not_consumed(self) -> None:
from ui.window_preset_file_drop import handle_native_preset_file_drop
window = SimpleNamespace(_preset_file_drop_filter=Mock())
with patch(
"ui.window_preset_file_drop.windows_dropped_file_paths",
return_value=None,
):
handled = handle_native_preset_file_drop(window, object())
self.assertFalse(handled)
window._preset_file_drop_filter.import_file_paths.assert_not_called()
def test_overlay_covers_whole_window_and_uses_selected_language(self) -> None:
from ui.window_preset_file_drop import PresetFileDropOverlay
from PyQt6.QtWidgets import QWidget
from qfluentwidgets import InfoBar
window = QWidget()
self.addCleanup(window.deleteLater)
window.resize(900, 600)
overlay = PresetFileDropOverlay(window, language_resolver=lambda: "en")
overlay.show_hint(["C:/Temp/My preset.txt"])
self.assertEqual(overlay.geometry(), window.rect())
self.assertFalse(overlay.isHidden())
self.assertIsInstance(overlay._info_bar, InfoBar)
self.assertEqual(overlay._title, "Drop the file to import")
self.assertEqual(overlay._subtitle, "My preset.txt")
self.assertEqual(overlay._info_bar.title, overlay._title)
self.assertEqual(overlay._info_bar.content, overlay._subtitle)
self.assertFalse(overlay._info_bar.titleLabel.isHidden())
self.assertFalse(overlay._info_bar.contentLabel.isHidden())
self.assertEqual(overlay._info_bar.accessibleName(), overlay._title)
self.assertEqual(overlay._info_bar.accessibleDescription(), overlay._subtitle)
self.assertNotIn("paintEvent", PresetFileDropOverlay.__dict__)
self.assertIsNone(overlay.graphicsEffect())
self.assertIsNotNone(overlay._backdrop.graphicsEffect())
self.assertIsNotNone(overlay._info_bar.graphicsEffect())
overlay.hide_immediately()
self.assertTrue(overlay.isHidden())
def test_overlay_flashes_accepted_message_and_auto_hides(self) -> None:
from ui.window_preset_file_drop import (
ACCEPTED_FLASH_DURATION_MS,
PresetFileDropOverlay,
)
from PyQt6.QtWidgets import QWidget
window = QWidget()
self.addCleanup(window.deleteLater)
window.resize(900, 600)
overlay = PresetFileDropOverlay(window, language_resolver=lambda: "en")
overlay.show_accepted(["C:/Temp/My preset.txt"])
self.assertFalse(overlay.isHidden())
self.assertEqual(overlay._title, "File accepted — importing…")
self.assertEqual(overlay._subtitle, "My preset.txt")
self.assertTrue(overlay._auto_hide_timer.isActive())
self.assertEqual(overlay._auto_hide_timer.interval(), ACCEPTED_FLASH_DURATION_MS)
# Наведение снова показывает подсказку и отменяет авто-скрытие броска.
overlay.show_hint(["C:/Temp/My preset.txt"])
self.assertFalse(overlay._auto_hide_timer.isActive())
self.assertEqual(overlay._title, "Drop the file to import")
overlay.hide_immediately()
self.assertTrue(overlay.isHidden())
self.assertFalse(overlay._auto_hide_timer.isActive())
def test_overlay_hover_hint_shows_generic_text_and_respects_accepted_flash(self) -> None:
from ui.window_preset_file_drop import PresetFileDropOverlay
from PyQt6.QtWidgets import QWidget
window = QWidget()
self.addCleanup(window.deleteLater)
window.resize(900, 600)
overlay = PresetFileDropOverlay(window, language_resolver=lambda: "en")
overlay.show_hover_hint()
self.assertFalse(overlay.isHidden())
self.assertEqual(overlay._title, "Drop the file to import")
self.assertEqual(overlay._subtitle, "TXT preset or ZIP archive")
self.assertFalse(overlay._auto_hide_timer.isActive())
# Пока показана плашка «файл принят», конец наведения её не гасит.
overlay.show_accepted(["C:/Temp/My preset.txt"])
overlay.hide_hover_hint()
self.assertFalse(overlay._hide_after_animation)
self.assertFalse(overlay.isHidden())
overlay._auto_hide_timer.stop()
overlay.hide_hover_hint()
self.assertTrue(overlay._hide_after_animation)
overlay.hide_immediately()
self.assertTrue(overlay.isHidden())
def test_filter_shows_hover_hint_only_for_import_capable_page(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
overlay = Mock()
importing_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: SimpleNamespace(
import_dropped_preset_files=Mock(return_value=True),
),
overlay=overlay,
)
self.assertTrue(importing_filter.show_hover_hint())
overlay.show_hover_hint.assert_called_once_with()
importing_filter.hide_hover_hint()
overlay.hide_hover_hint.assert_called_once_with()
ordinary_overlay = Mock()
ordinary_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: object(),
overlay=ordinary_overlay,
)
self.assertFalse(ordinary_filter.show_hover_hint())
ordinary_overlay.show_hover_hint.assert_not_called()
def test_main_window_creates_drag_hover_detector(self) -> None:
from ui.fluent_app_window import ZapretFluentWindow
from ui.windows_drag_hover_detector import WindowsDragHoverDetector
with patch(
"ui.fluent_app_window.enable_windows_file_drop",
return_value=True,
):
window = ZapretFluentWindow()
self.addCleanup(window.deleteLater)
event_filter = window._preset_file_drop_filter
self.addCleanup(self._app.removeEventFilter, event_filter)
detector = window._drag_hover_detector
self.assertIsInstance(detector, WindowsDragHoverDetector)
self.assertIs(detector._on_hover_start.__self__, event_filter)
self.assertIs(detector._on_hover_end.__self__, event_filter)
def test_failed_import_hides_overlay_without_accepted_flash(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Rejected.txt"
preset_path.write_text("preset", encoding="utf-8")
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=False))
overlay = Mock()
event_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: target,
overlay=overlay,
)
imported = event_filter.import_file_paths([str(preset_path)])
self.assertFalse(imported)
overlay.show_accepted.assert_not_called()
overlay.hide_hint.assert_called_once_with()
def test_filter_ignores_other_windows_and_pages_without_import_action(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Dragged.txt"
preset_path.write_text("preset", encoding="utf-8")
event = _DropEvent(QEvent.Type.Drop, [QUrl.fromLocalFile(str(preset_path))])
window = object()
other_window_filter = WindowPresetFileDropFilter(
window,
target_resolver=lambda: SimpleNamespace(import_dropped_preset_files=Mock()),
)
self.assertFalse(other_window_filter.eventFilter(_Receiver(object()), event))
ordinary_page_filter = WindowPresetFileDropFilter(
window,
target_resolver=lambda: object(),
)
self.assertFalse(ordinary_page_filter.eventFilter(_Receiver(window), event))
self.assertFalse(event.accepted)
def test_user_presets_page_queues_every_dropped_file(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._request_preset_bulk_action = Mock(return_value=True)
result = UserPresetsPageBase.import_dropped_preset_files(
page,
["C:/Temp/First.txt", "C:/Temp/Second.ZIP"],
)
self.assertTrue(result)
self.assertEqual(
page._request_preset_bulk_action.call_args_list,
[
unittest.mock.call("import", file_path="C:/Temp/First.txt"),
unittest.mock.call("import", file_path="C:/Temp/Second.ZIP"),
],
)
def test_drop_delegate_takes_over_drop_and_suppresses_overlay(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Delegated.txt"
preset_path.write_text("preset", encoding="utf-8")
url = QUrl.fromLocalFile(str(preset_path))
window = object()
receiver = _Receiver(window)
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
overlay = Mock()
event_filter = WindowPresetFileDropFilter(
window,
target_resolver=lambda: target,
overlay=overlay,
)
delegate = SimpleNamespace(handle_dropped_preset_files=Mock(return_value=True))
event_filter.set_drop_delegate(delegate)
drag_event = _DropEvent(QEvent.Type.DragEnter, [url])
self.assertTrue(event_filter.eventFilter(receiver, drag_event))
overlay.show_hint.assert_not_called()
drop_event = _DropEvent(QEvent.Type.Drop, [url])
self.assertTrue(event_filter.eventFilter(receiver, drop_event))
delegate.handle_dropped_preset_files.assert_called_once_with([str(preset_path)])
target.import_dropped_preset_files.assert_not_called()
overlay.show_accepted.assert_not_called()
event_filter.set_drop_delegate(None)
second_drop = _DropEvent(QEvent.Type.Drop, [url])
self.assertTrue(event_filter.eventFilter(receiver, second_drop))
target.import_dropped_preset_files.assert_called_once_with([str(preset_path)])
def test_native_drop_respects_drop_delegate(self) -> None:
from ui.window_preset_file_drop import (
WindowPresetFileDropFilter,
handle_native_preset_file_drop,
)
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "NativeDelegated.txt"
preset_path.write_text("preset", encoding="utf-8")
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
event_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: target,
overlay=Mock(),
)
delegate = SimpleNamespace(handle_dropped_preset_files=Mock(return_value=True))
event_filter.set_drop_delegate(delegate)
window = SimpleNamespace(_preset_file_drop_filter=event_filter)
with patch(
"ui.window_preset_file_drop.windows_dropped_file_paths",
return_value=[str(preset_path)],
):
handled = handle_native_preset_file_drop(window, object())
self.assertTrue(handled)
delegate.handle_dropped_preset_files.assert_called_once_with([str(preset_path)])
target.import_dropped_preset_files.assert_not_called()
def test_dead_delegate_falls_back_to_page(self) -> None:
from ui.window_preset_file_drop import WindowPresetFileDropFilter
class _Delegate:
def handle_dropped_preset_files(self, paths):
return True
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "Fallback.txt"
preset_path.write_text("preset", encoding="utf-8")
target = SimpleNamespace(import_dropped_preset_files=Mock(return_value=True))
event_filter = WindowPresetFileDropFilter(
object(),
target_resolver=lambda: target,
overlay=Mock(),
)
delegate = _Delegate()
event_filter.set_drop_delegate(delegate)
del delegate # weakref фильтра должен отпустить удалённый диалог
self.assertTrue(event_filter.import_file_paths([str(preset_path)]))
target.import_dropped_preset_files.assert_called_once_with([str(preset_path)])
if __name__ == "__main__":
unittest.main()