372 lines
13 KiB
Python
372 lines
13 KiB
Python
"""
|
|
Tests for ElementManipulationMixin
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, MagicMock
|
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|
from pyPhotoAlbum.mixins.element_manipulation import ElementManipulationMixin
|
|
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
|
|
from pyPhotoAlbum.models import ImageData
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
|
|
# Create test widget combining necessary mixins
|
|
class TestManipulationWidget(ElementManipulationMixin, ElementSelectionMixin, QOpenGLWidget):
|
|
"""Test widget combining manipulation and selection mixins"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._page_renderers = []
|
|
self.drag_start_pos = None
|
|
self.drag_start_element_pos = None
|
|
|
|
|
|
class TestElementManipulationInitialization:
|
|
"""Test ElementManipulationMixin initialization"""
|
|
|
|
def test_initialization_sets_defaults(self, qtbot):
|
|
"""Test that mixin initializes with correct defaults"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
assert widget.resize_handle is None
|
|
assert widget.resize_start_pos is None
|
|
assert widget.resize_start_size is None
|
|
assert widget.rotation_mode is False
|
|
assert widget.rotation_start_angle is None
|
|
assert widget.rotation_snap_angle == 15
|
|
assert widget.snap_state == {
|
|
'is_snapped': False,
|
|
'last_position': None,
|
|
'last_size': None
|
|
}
|
|
|
|
def test_rotation_mode_is_mutable(self, qtbot):
|
|
"""Test that rotation mode can be toggled"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
widget.rotation_mode = True
|
|
assert widget.rotation_mode is True
|
|
|
|
widget.rotation_mode = False
|
|
assert widget.rotation_mode is False
|
|
|
|
def test_rotation_snap_angle_is_configurable(self, qtbot):
|
|
"""Test that rotation snap angle can be changed"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
widget.rotation_snap_angle = 45
|
|
assert widget.rotation_snap_angle == 45
|
|
|
|
|
|
class TestResizeElementNoSnap:
|
|
"""Test _resize_element_no_snap method"""
|
|
|
|
def test_resize_se_handle_increases_size(self, qtbot):
|
|
"""Test SE handle resizes from bottom-right"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# Drag 50 pixels right and down
|
|
widget._resize_element_no_snap(50, 30)
|
|
|
|
assert elem.position == (100, 100) # Position unchanged
|
|
assert elem.size == (250, 180) # Size increased
|
|
|
|
def test_resize_nw_handle_moves_and_resizes(self, qtbot):
|
|
"""Test NW handle moves position and adjusts size"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'nw'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# Drag 20 pixels left and up (negative deltas in local coordinates mean expansion)
|
|
widget._resize_element_no_snap(-20, -10)
|
|
|
|
assert elem.position == (80, 90) # Moved up-left
|
|
assert elem.size == (220, 160) # Size increased
|
|
|
|
def test_resize_ne_handle(self, qtbot):
|
|
"""Test NE handle behavior"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'ne'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# Drag right and up
|
|
widget._resize_element_no_snap(30, -20)
|
|
|
|
assert elem.position == (100, 80) # Y moved up, X unchanged
|
|
assert elem.size == (230, 170) # Both dimensions increased
|
|
|
|
def test_resize_sw_handle(self, qtbot):
|
|
"""Test SW handle behavior"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'sw'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# Drag left and down
|
|
widget._resize_element_no_snap(-15, 25)
|
|
|
|
assert elem.position == (85, 100) # X moved left, Y unchanged
|
|
assert elem.size == (215, 175) # Both dimensions increased
|
|
|
|
def test_resize_enforces_minimum_size(self, qtbot):
|
|
"""Test that resize enforces minimum size of 20px"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=50, height=50)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (50, 50)
|
|
|
|
# Try to shrink below minimum
|
|
widget._resize_element_no_snap(-40, -40)
|
|
|
|
assert elem.size[0] >= 20 # Width at least 20
|
|
assert elem.size[1] >= 20 # Height at least 20
|
|
|
|
def test_resize_no_op_without_resize_start(self, qtbot):
|
|
"""Test resize does nothing without start position/size"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
# Don't set resize_start_pos or resize_start_size
|
|
|
|
original_pos = elem.position
|
|
original_size = elem.size
|
|
|
|
widget._resize_element_no_snap(50, 50)
|
|
|
|
# Should be unchanged
|
|
assert elem.position == original_pos
|
|
assert elem.size == original_size
|
|
|
|
|
|
class TestResizeElementWithSnap:
|
|
"""Test _resize_element method with snapping"""
|
|
|
|
def test_resize_with_snap_calls_snapping_system(self, qtbot):
|
|
"""Test resize with snap uses snapping system"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
# Create element with parent page
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
page.layout.add_element(elem)
|
|
elem._parent_page = page
|
|
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# Mock window and project
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
mock_window.project.working_dpi = 96
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# Mock snap_resize to return modified values
|
|
mock_snap_sys = page.layout.snapping_system
|
|
mock_snap_sys.snap_resize = Mock(return_value=((100, 100), (250, 180)))
|
|
|
|
widget._resize_element(50, 30)
|
|
|
|
# Verify snap_resize was called
|
|
assert mock_snap_sys.snap_resize.called
|
|
call_args = mock_snap_sys.snap_resize.call_args
|
|
assert call_args[1]['dx'] == 50
|
|
assert call_args[1]['dy'] == 30
|
|
assert call_args[1]['resize_handle'] == 'se'
|
|
|
|
# Verify element was updated
|
|
assert elem.size == (250, 180)
|
|
|
|
def test_resize_without_parent_page_uses_no_snap(self, qtbot):
|
|
"""Test resize without parent page falls back to no-snap"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (200, 150)
|
|
|
|
# No _parent_page attribute
|
|
widget._resize_element(50, 30)
|
|
|
|
# Should use no-snap logic
|
|
assert elem.size == (250, 180)
|
|
|
|
def test_resize_enforces_minimum_size_with_snap(self, qtbot):
|
|
"""Test minimum size is enforced even with snapping"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=50, height=50)
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
page.layout.add_element(elem)
|
|
elem._parent_page = page
|
|
|
|
widget.selected_element = elem
|
|
widget.resize_handle = 'se'
|
|
widget.resize_start_pos = (100, 100)
|
|
widget.resize_start_size = (50, 50)
|
|
|
|
# Mock window
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
mock_window.project.working_dpi = 96
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# Mock snap to return tiny size
|
|
mock_snap_sys = page.layout.snapping_system
|
|
mock_snap_sys.snap_resize = Mock(return_value=((100, 100), (5, 5)))
|
|
|
|
widget._resize_element(-45, -45)
|
|
|
|
# Should enforce minimum
|
|
assert elem.size[0] >= 20
|
|
assert elem.size[1] >= 20
|
|
|
|
|
|
class TestTransferElementToPage:
|
|
"""Test _transfer_element_to_page method"""
|
|
|
|
def test_transfer_moves_element_between_pages(self, qtbot):
|
|
"""Test element is transferred from source to target page"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
# Create source and target pages
|
|
source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)
|
|
|
|
# Create element on source page
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
source_page.layout.add_element(elem)
|
|
|
|
# Mock renderer
|
|
mock_renderer = Mock()
|
|
mock_renderer.screen_to_page = Mock(return_value=(150, 175))
|
|
|
|
# Transfer element
|
|
widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)
|
|
|
|
# Verify element removed from source
|
|
assert elem not in source_page.layout.elements
|
|
|
|
# Verify element added to target
|
|
assert elem in target_page.layout.elements
|
|
|
|
# Verify element references updated
|
|
assert elem._parent_page is target_page
|
|
assert elem._page_renderer is mock_renderer
|
|
|
|
def test_transfer_centers_element_on_mouse(self, qtbot):
|
|
"""Test transferred element is centered on mouse position"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
source_page.layout.add_element(elem)
|
|
|
|
# Mock renderer - mouse at (250, 300) screen -> (150, 175) page
|
|
mock_renderer = Mock()
|
|
mock_renderer.screen_to_page = Mock(return_value=(150, 175))
|
|
|
|
widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)
|
|
|
|
# Element should be centered: (150 - 200/2, 175 - 150/2) = (50, 100)
|
|
assert elem.position == (50, 100)
|
|
|
|
def test_transfer_updates_drag_state(self, qtbot):
|
|
"""Test transfer updates drag start position"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
source_page.layout.add_element(elem)
|
|
|
|
mock_renderer = Mock()
|
|
mock_renderer.screen_to_page = Mock(return_value=(150, 175))
|
|
|
|
widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)
|
|
|
|
# Drag state should be updated
|
|
assert widget.drag_start_pos == (250, 300)
|
|
assert widget.drag_start_element_pos == elem.position
|
|
|
|
|
|
class TestManipulationStateManagement:
|
|
"""Test state management"""
|
|
|
|
def test_snap_state_dictionary_structure(self, qtbot):
|
|
"""Test snap state has expected structure"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
assert 'is_snapped' in widget.snap_state
|
|
assert 'last_position' in widget.snap_state
|
|
assert 'last_size' in widget.snap_state
|
|
|
|
def test_resize_state_can_be_set(self, qtbot):
|
|
"""Test resize state variables can be set"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
widget.resize_handle = 'nw'
|
|
widget.resize_start_pos = (10, 20)
|
|
widget.resize_start_size = (100, 200)
|
|
|
|
assert widget.resize_handle == 'nw'
|
|
assert widget.resize_start_pos == (10, 20)
|
|
assert widget.resize_start_size == (100, 200)
|
|
|
|
def test_rotation_state_can_be_set(self, qtbot):
|
|
"""Test rotation state variables can be set"""
|
|
widget = TestManipulationWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
widget.rotation_mode = True
|
|
widget.rotation_start_angle = 45.0
|
|
|
|
assert widget.rotation_mode is True
|
|
assert widget.rotation_start_angle == 45.0
|