""" Tests for MouseInteractionMixin """ import pytest from unittest.mock import Mock, MagicMock, patch from PyQt6.QtCore import Qt, QPoint, QPointF from PyQt6.QtGui import QMouseEvent, QWheelEvent from PyQt6.QtOpenGLWidgets import QOpenGLWidget from pyPhotoAlbum.mixins.mouse_interaction import MouseInteractionMixin from pyPhotoAlbum.mixins.viewport import ViewportMixin from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin from pyPhotoAlbum.mixins.element_manipulation import ElementManipulationMixin from pyPhotoAlbum.mixins.image_pan import ImagePanMixin from pyPhotoAlbum.mixins.page_navigation import PageNavigationMixin from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.models import ImageData, TextBoxData # Create test widget combining necessary mixins class TestMouseInteractionWidget( MouseInteractionMixin, PageNavigationMixin, ImagePanMixin, ElementManipulationMixin, ElementSelectionMixin, ViewportMixin, UndoableInteractionMixin, QOpenGLWidget ): """Test widget combining mouse interaction with other required mixins""" def __init__(self): super().__init__() # Initialize additional state not covered by mixins self.current_page_index = 0 self.resize_handle = None self.rotation_snap_angle = 15 class TestMouseInteractionInitialization: """Test MouseInteractionMixin initialization""" def test_widget_initializes_state(self, qtbot): """Test that widget initializes mouse interaction state""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) # Should have initialized state assert hasattr(widget, 'drag_start_pos') assert hasattr(widget, 'is_dragging') assert hasattr(widget, 'is_panning') assert widget.drag_start_pos is None assert widget.is_dragging is False assert widget.is_panning is False class TestMousePressEvent: """Test mousePressEvent method""" def test_left_click_starts_drag(self, qtbot): """Test left click starts drag operation - clears selection on empty click""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) # Mock update method widget.update = Mock() # Create left click event event = Mock() event.button = Mock(return_value=Qt.MouseButton.LeftButton) event.position = Mock(return_value=QPointF(100, 100)) event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) # Mock element selection and ghost page check widget._get_element_at = Mock(return_value=None) widget._get_page_at = Mock(return_value=(None, -1, None)) widget._check_ghost_page_click = Mock(return_value=False) widget.mousePressEvent(event) # Should clear selection when clicking on empty space assert len(widget.selected_elements) == 0 assert widget.update.called def test_left_click_selects_element(self, qtbot): """Test left click on element selects it""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() # Create mock element mock_element = ImageData(image_path="/test.jpg", x=50, y=50, width=100, height=100) event = Mock() event.button = Mock(return_value=Qt.MouseButton.LeftButton) event.position = Mock(return_value=QPointF(75, 75)) event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) # Mock element selection to return the element widget._get_element_at = Mock(return_value=mock_element) widget._get_page_at = Mock(return_value=(None, -1, None)) widget._check_ghost_page_click = Mock(return_value=False) widget.mousePressEvent(event) # Should select the element assert mock_element in widget.selected_elements assert widget.drag_start_pos == (75, 75) assert widget.is_dragging is True def test_ctrl_click_image_enters_image_pan_mode(self, qtbot): """Test Ctrl+click on ImageData enters image pan mode""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() widget.setCursor = Mock() # Create image element with crop info element = ImageData( image_path="/test.jpg", x=50, y=50, width=100, height=100, crop_info={'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0} ) event = Mock() event.button = Mock(return_value=Qt.MouseButton.LeftButton) event.position = Mock(return_value=QPointF(75, 75)) event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier) # Mock element selection to return the image element widget._get_element_at = Mock(return_value=element) widget._get_page_at = Mock(return_value=(None, -1, None)) widget._check_ghost_page_click = Mock(return_value=False) widget.mousePressEvent(event) # Should enter image pan mode assert element in widget.selected_elements assert widget.image_pan_mode is True assert widget.is_dragging is True assert widget.setCursor.called def test_middle_click_starts_panning(self, qtbot): """Test middle mouse button starts panning""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() event = Mock() event.button = Mock(return_value=Qt.MouseButton.MiddleButton) event.position = Mock(return_value=QPointF(150, 150)) widget.mousePressEvent(event) # Should start panning assert widget.is_panning is True assert widget.drag_start_pos == (150, 150) def test_click_on_ghost_page_adds_page(self, qtbot): """Test clicking on ghost page calls check method""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() # Mock ghost page click check to return True (handled ghost click) widget._check_ghost_page_click = Mock(return_value=True) event = Mock() event.button = Mock(return_value=Qt.MouseButton.LeftButton) event.position = Mock(return_value=QPointF(100, 100)) event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) widget.mousePressEvent(event) # Should have called check_ghost_page_click assert widget._check_ghost_page_click.called # Ghost click returns early, so update should not be called assert not widget.update.called class TestMouseMoveEvent: """Test mouseMoveEvent method""" def test_hover_shows_resize_cursor(self, qtbot): """Test hovering over resize handle (not dragging)""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() widget._update_page_status = Mock() # Create selected element element = ImageData(image_path="/test.jpg", x=50, y=50, width=100, height=100) widget.selected_elements.add(element) event = Mock() event.position = Mock(return_value=QPointF(150, 150)) # Bottom-right corner event.buttons = Mock(return_value=Qt.MouseButton.NoButton) # Mock resize handle detection widget._get_resize_handle_at = Mock(return_value='bottom-right') widget._get_element_at = Mock(return_value=element) widget.mouseMoveEvent(event) # Should call _update_page_status but not update (no drag) assert widget._update_page_status.called # No dragging, so no update call assert not widget.update.called def test_drag_moves_element(self, qtbot): """Test dragging moves selected element""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() widget._update_page_status = Mock() # Setup project 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] widget.window = Mock(return_value=mock_window) # Create and select element element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100) page.layout.add_element(element) widget.selected_elements.add(element) element._parent_page = page # Start drag widget.drag_start_pos = (150, 150) widget.is_dragging = True widget.drag_start_element_pos = (100, 100) # Mock page detection mock_renderer = Mock() mock_renderer.screen_to_page = Mock(return_value=(180, 180)) widget._get_page_at = Mock(return_value=(page, 0, mock_renderer)) event = Mock() event.position = Mock(return_value=QPointF(180, 180)) event.buttons = Mock(return_value=Qt.MouseButton.LeftButton) event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) widget.mouseMoveEvent(event) # Element should have moved (with snapping, so position should change) assert element.position != (100, 100) assert widget.update.called def test_middle_button_panning(self, qtbot): """Test middle button drag pans viewport""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() # Start panning widget.is_panning = True widget.drag_start_pos = (100, 100) initial_pan = widget.pan_offset.copy() event = Mock() event.position = Mock(return_value=QPointF(150, 150)) event.buttons = Mock(return_value=Qt.MouseButton.MiddleButton) widget.mouseMoveEvent(event) # Pan offset should have changed assert widget.pan_offset != initial_pan assert widget.update.called def test_ctrl_drag_pans_image_in_frame(self, qtbot): """Test Ctrl+drag pans image within frame""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() widget._update_page_status = Mock() # Create image element with crop info element = ImageData( image_path="/test.jpg", x=100, y=100, width=100, height=100, crop_info={'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0} ) widget.selected_elements.add(element) # Start image pan drag widget.drag_start_pos = (150, 150) widget.is_dragging = True widget.image_pan_mode = True widget._image_pan_start = (0.0, 0.0) event = Mock() event.position = Mock(return_value=QPointF(160, 160)) event.buttons = Mock(return_value=Qt.MouseButton.LeftButton) event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier) # Mock _handle_image_pan_move method widget._handle_image_pan_move = Mock() widget.mouseMoveEvent(event) # Should call _handle_image_pan_move assert widget._handle_image_pan_move.called class TestMouseReleaseEvent: """Test mouseReleaseEvent method""" def test_release_clears_drag_state(self, qtbot): """Test mouse release clears drag state""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.setCursor = Mock() # Setup drag state widget.is_dragging = True widget.drag_start_pos = (100, 100) event = Mock() event.button = Mock(return_value=Qt.MouseButton.LeftButton) widget.mouseReleaseEvent(event) # Should clear drag state assert widget.is_dragging is False assert widget.drag_start_pos is None assert widget.setCursor.called def test_release_clears_panning_state(self, qtbot): """Test mouse release clears panning state""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() # Setup panning state widget.is_panning = True widget.drag_start_pos = (100, 100) event = Mock() event.button = Mock(return_value=Qt.MouseButton.MiddleButton) widget.mouseReleaseEvent(event) # Should clear panning state assert widget.is_panning is False assert widget.drag_start_pos is None class TestMouseDoubleClickEvent: """Test mouseDoubleClickEvent method""" def test_double_click_text_starts_editing(self, qtbot): """Test double-clicking text element starts editing""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) # Create text element with correct constructor text_element = TextBoxData( text_content="Test", x=100, y=100, width=100, height=50, font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)} ) # Mock _edit_text_element method widget._edit_text_element = Mock() # Mock element selection to return text element widget._get_element_at = Mock(return_value=text_element) # Create real event (not Mock) for button() event = QMouseEvent( QMouseEvent.Type.MouseButtonDblClick, QPointF(125, 125), Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier ) widget.mouseDoubleClickEvent(event) # Should call _edit_text_element widget._edit_text_element.assert_called_once_with(text_element) def test_double_click_non_text_does_nothing(self, qtbot): """Test double-clicking non-text element does nothing""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) # Create image element (not text) element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100) widget._edit_text_element = Mock() widget._get_element_at = Mock(return_value=element) # Create real event (not Mock) for button() event = QMouseEvent( QMouseEvent.Type.MouseButtonDblClick, QPointF(125, 125), Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier ) widget.mouseDoubleClickEvent(event) # Should not call _edit_text_element assert not widget._edit_text_element.called class TestWheelEvent: """Test wheelEvent method""" def test_scroll_pans_viewport(self, qtbot): """Test scroll wheel pans viewport without Ctrl""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() initial_pan = widget.pan_offset[1] event = Mock() event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=-120))) # Scroll down event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) widget.wheelEvent(event) # Should pan viewport assert widget.pan_offset[1] != initial_pan assert widget.update.called def test_ctrl_scroll_zooms(self, qtbot): """Test Ctrl+scroll zooms viewport""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() initial_zoom = widget.zoom_level event = Mock() event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=120))) # Scroll up event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier) event.position = Mock(return_value=QPointF(100, 100)) widget.wheelEvent(event) # Should zoom in assert widget.zoom_level > initial_zoom assert widget.update.called def test_scroll_up_pans_viewport_up(self, qtbot): """Test scrolling up pans viewport upward""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() initial_pan = widget.pan_offset[1] event = Mock() event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=120))) # Scroll up event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) widget.wheelEvent(event) # Should pan viewport up (increase pan_offset[1]) assert widget.pan_offset[1] > initial_pan assert widget.update.called def test_scroll_down_pans_viewport_down(self, qtbot): """Test scrolling down pans viewport downward""" widget = TestMouseInteractionWidget() qtbot.addWidget(widget) widget.update = Mock() initial_pan = widget.pan_offset[1] event = Mock() event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=-120))) # Scroll down event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier) widget.wheelEvent(event) # Should pan viewport down (decrease pan_offset[1]) assert widget.pan_offset[1] < initial_pan assert widget.update.called