""" 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