pyPhotoAlbum/tests/test_asset_drop_mixin.py
Duncan Tourolle 7f32858baf
All checks were successful
Python CI / test (push) Successful in 1m7s
Lint / lint (push) Successful in 1m11s
Tests / test (3.10) (push) Successful in 50s
Tests / test (3.11) (push) Successful in 51s
Tests / test (3.9) (push) Successful in 47s
big refactor to use mixin architecture
2025-11-11 10:35:24 +01:00

345 lines
12 KiB
Python

"""
Tests for AssetDropMixin
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtCore import QMimeData, QUrl, QPoint
from PyQt6.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.asset_drop import AssetDropMixin
from pyPhotoAlbum.mixins.viewport import ViewportMixin
from pyPhotoAlbum.mixins.page_navigation import PageNavigationMixin
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData
# Create test widget combining necessary mixins
class TestAssetDropWidget(AssetDropMixin, PageNavigationMixin, ViewportMixin, QOpenGLWidget):
"""Test widget combining asset drop, page navigation, and viewport mixins"""
def _get_element_at(self, x, y):
"""Mock implementation for testing"""
# Will be overridden in tests that need it
return None
class TestAssetDropInitialization:
"""Test AssetDropMixin initialization"""
def test_widget_accepts_drops(self, qtbot):
"""Test that widget is configured to accept drops"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
# Should accept drops (set in GLWidget.__init__)
# This is a property of the widget, not the mixin
assert hasattr(widget, 'acceptDrops')
class TestDragEnterEvent:
"""Test dragEnterEvent method"""
def test_accepts_image_urls(self, qtbot):
"""Test accepts drag events with image file URLs"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
# Create mime data with image file
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
# Create drag enter event
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.acceptProposedAction = Mock()
widget.dragEnterEvent(event)
# Should accept the event
assert event.acceptProposedAction.called
def test_accepts_png_files(self, qtbot):
"""Test accepts PNG files"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.png")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.acceptProposedAction = Mock()
widget.dragEnterEvent(event)
assert event.acceptProposedAction.called
def test_rejects_non_image_files(self, qtbot):
"""Test rejects non-image files"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/document.pdf")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.acceptProposedAction = Mock()
event.ignore = Mock()
widget.dragEnterEvent(event)
# Should not accept PDF files
assert not event.acceptProposedAction.called
def test_rejects_empty_mime_data(self, qtbot):
"""Test rejects events with no URLs"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
mime_data = QMimeData()
# No URLs set
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.acceptProposedAction = Mock()
widget.dragEnterEvent(event)
assert not event.acceptProposedAction.called
class TestDragMoveEvent:
"""Test dragMoveEvent method"""
def test_accepts_drag_move_with_image(self, qtbot):
"""Test accepts drag move events with image files"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.acceptProposedAction = Mock()
widget.dragMoveEvent(event)
assert event.acceptProposedAction.called
class TestDropEvent:
"""Test dropEvent method"""
@patch('pyPhotoAlbum.mixins.asset_drop.AddElementCommand')
def test_drop_creates_image_element(self, mock_cmd_class, qtbot):
"""Test dropping image file creates ImageData element"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
widget.zoom_level = 1.0
widget.pan_offset = [0, 0]
# Mock update method
widget.update = Mock()
# Setup project with page
mock_window = Mock()
mock_window.project = Project(name="Test")
mock_window.project.working_dpi = 96
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
mock_window.project.pages = [page]
# Mock asset manager
mock_window.project.asset_manager = Mock()
mock_window.project.asset_manager.import_asset = Mock(return_value="/imported/image.jpg")
# Mock history
mock_window.project.history = Mock()
# Mock page renderer
mock_renderer = Mock()
mock_renderer.is_point_in_page = Mock(return_value=True)
mock_renderer.screen_to_page = Mock(return_value=(100, 100))
# Mock _get_page_at to return tuple
widget._get_page_at = Mock(return_value=(page, 0, mock_renderer))
widget._page_renderers = [(mock_renderer, page)]
widget.window = Mock(return_value=mock_window)
# Create drop event
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.position = Mock(return_value=QPoint(150, 150))
event.acceptProposedAction = Mock()
widget.dropEvent(event)
# Should have called asset manager
assert mock_window.project.asset_manager.import_asset.called
# Should have created command
assert mock_cmd_class.called
# Should have executed command
assert mock_window.project.history.execute.called
assert widget.update.called
def test_drop_outside_page_does_nothing(self, qtbot):
"""Test dropping outside any page does nothing"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
widget.zoom_level = 1.0
widget.pan_offset = [0, 0]
mock_window = Mock()
mock_window.project = Project(name="Test")
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
mock_window.project.pages = [page]
# Mock renderer that returns False (not in page)
mock_renderer = Mock()
mock_renderer.is_point_in_page = Mock(return_value=False)
widget._page_renderers = [(mock_renderer, page)]
widget.window = Mock(return_value=mock_window)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.position = Mock(return_value=QPoint(5000, 5000))
event.acceptProposedAction = Mock()
widget.dropEvent(event)
# Should not create any elements
assert len(page.layout.elements) == 0
def test_drop_updates_existing_placeholder(self, qtbot):
"""Test dropping on existing placeholder updates it with image"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
widget.zoom_level = 1.0
widget.pan_offset = [0, 0]
widget.update = Mock()
# Setup project with page containing placeholder
mock_window = Mock()
mock_window.project = Project(name="Test")
mock_window.project.working_dpi = 96
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
from pyPhotoAlbum.models import PlaceholderData
placeholder = PlaceholderData(x=100, y=100, width=200, height=150)
page.layout.elements.append(placeholder)
mock_window.project.pages = [page]
# Mock renderer
mock_renderer = Mock()
mock_renderer.is_point_in_page = Mock(return_value=True)
mock_renderer.screen_to_page = Mock(return_value=(150, 150))
widget._page_renderers = [(mock_renderer, page)]
widget.window = Mock(return_value=mock_window)
# Mock element selection to return the placeholder
widget._get_element_at = Mock(return_value=placeholder)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.position = Mock(return_value=QPoint(150, 150))
event.acceptProposedAction = Mock()
widget.dropEvent(event)
# Should replace placeholder with ImageData
assert len(page.layout.elements) == 1
assert isinstance(page.layout.elements[0], ImageData)
assert page.layout.elements[0].image_path == "/path/to/image.jpg"
@patch('pyPhotoAlbum.mixins.asset_drop.AddElementCommand')
def test_drop_multiple_files(self, mock_cmd_class, qtbot):
"""Test dropping first image from multiple files"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
widget.zoom_level = 1.0
widget.pan_offset = [0, 0]
widget.update = Mock()
mock_window = Mock()
mock_window.project = Project(name="Test")
mock_window.project.working_dpi = 96
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
mock_window.project.pages = [page]
mock_window.project.asset_manager = Mock()
mock_window.project.asset_manager.import_asset = Mock(return_value="/imported/image1.jpg")
mock_window.project.history = Mock()
mock_renderer = Mock()
mock_renderer.is_point_in_page = Mock(return_value=True)
mock_renderer.screen_to_page = Mock(return_value=(100, 100))
widget._get_page_at = Mock(return_value=(page, 0, mock_renderer))
widget._page_renderers = [(mock_renderer, page)]
widget.window = Mock(return_value=mock_window)
# Create drop event with multiple files (only first is used)
mime_data = QMimeData()
mime_data.setUrls([
QUrl.fromLocalFile("/path/to/image1.jpg"),
QUrl.fromLocalFile("/path/to/image2.png"),
QUrl.fromLocalFile("/path/to/image3.jpg")
])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.position = Mock(return_value=QPoint(150, 150))
event.acceptProposedAction = Mock()
widget.dropEvent(event)
# Only first image should be processed
assert mock_window.project.asset_manager.import_asset.call_count == 1
def test_drop_no_project_does_nothing(self, qtbot):
"""Test dropping when no project loaded does nothing"""
widget = TestAssetDropWidget()
qtbot.addWidget(widget)
widget.update = Mock()
mock_window = Mock()
mock_window.project = None
widget.window = Mock(return_value=mock_window)
# Mock _get_element_at to return None (no element hit)
widget._get_element_at = Mock(return_value=None)
mime_data = QMimeData()
mime_data.setUrls([QUrl.fromLocalFile("/path/to/image.jpg")])
event = Mock()
event.mimeData = Mock(return_value=mime_data)
event.position = Mock(return_value=QPoint(150, 150))
event.acceptProposedAction = Mock()
# Should not crash
widget.dropEvent(event)
# Should still accept event and call update
assert event.acceptProposedAction.called
assert widget.update.called