pyPhotoAlbum/tests/test_mouse_interaction_mixin.py
Duncan Tourolle a7f32e73b9
All checks were successful
Python CI / test (push) Successful in 1m31s
Lint / lint (push) Successful in 1m8s
Tests / test (3.11) (push) Successful in 1m42s
Tests / test (3.12) (push) Successful in 1m43s
Tests / test (3.13) (push) Successful in 1m37s
Tests / test (3.14) (push) Successful in 1m16s
pan with two finger scroll
2025-12-02 20:57:37 +01:00

986 lines
34 KiB
Python
Executable File

"""
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=(0.0, 0.0, 1.0, 1.0), # crop_info is a tuple (x, y, width, height)
)
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()
widget.clamp_pan_offset = Mock() # Mock clamping to allow any pan offset
# 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.pan_offset == [50.0, 50.0] # Moved by 50 pixels in each direction
assert widget.update.called
assert widget.clamp_pan_offset.called # Clamping should be 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=(0.0, 0.0, 1.0, 1.0), # crop_info is a tuple (x, y, width, height)
)
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(x=Mock(return_value=0), 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()
# Mock clamp_pan_offset to prevent it from resetting pan_offset
widget.clamp_pan_offset = Mock()
initial_pan = widget.pan_offset[1]
event = Mock()
event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), 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()
# Mock clamp_pan_offset to prevent it from resetting pan_offset
widget.clamp_pan_offset = Mock()
initial_pan = widget.pan_offset[1]
event = Mock()
event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), 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
class TestRotationMode:
"""Test rotation mode functionality"""
def test_click_in_rotation_mode_starts_rotation(self, qtbot):
"""Test clicking on element in rotation mode starts rotation"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget.rotation_mode = True
# Create and select element
element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
widget.selected_elements.add(element)
# Mock the _begin_rotate method
widget._begin_rotate = Mock()
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(150, 150))
event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)
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 start rotation
assert widget.is_dragging is True
assert widget.drag_start_pos == (150, 150)
assert hasattr(widget, "rotation_start_angle")
widget._begin_rotate.assert_called_once_with(element)
def test_mouse_move_in_rotation_mode(self, qtbot):
"""Test mouse move in rotation mode rotates element"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget._update_page_status = Mock()
widget.rotation_mode = True
# Create element with page renderer
element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
element.rotation = 0
widget.selected_elements.add(element)
# Create mock renderer
mock_renderer = Mock()
mock_renderer.page_to_screen = Mock(return_value=(150, 150)) # Center of element
element._page_renderer = mock_renderer
# Start dragging in rotation mode
widget.drag_start_pos = (150, 150)
widget.is_dragging = True
widget.rotation_start_angle = 0
# Mock window with show_status
mock_window = Mock()
mock_window.show_status = Mock()
widget.window = Mock(return_value=mock_window)
event = Mock()
event.position = Mock(return_value=QPointF(200, 150)) # Mouse to the right
widget.mouseMoveEvent(event)
# Rotation should be updated (0 degrees for right side)
assert element.rotation == 0 # Snapped to nearest 15 degrees
assert widget.update.called
mock_window.show_status.assert_called()
class TestResizeMode:
"""Test resize mode functionality"""
def test_click_on_resize_handle_starts_resize(self, qtbot):
"""Test clicking on resize handle starts resize"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget.rotation_mode = False
# Create and select element
element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
widget.selected_elements.add(element)
# Mock the _begin_resize method and _get_resize_handle_at
widget._begin_resize = Mock()
widget._get_resize_handle_at = Mock(return_value="bottom-right")
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(200, 200)) # Bottom-right corner
event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)
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 start resize
assert widget.is_dragging is True
assert widget.resize_handle == "bottom-right"
assert widget.drag_start_pos == (200, 200)
widget._begin_resize.assert_called_once_with(element)
def test_mouse_move_in_resize_mode(self, qtbot):
"""Test mouse move in resize mode resizes element"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget._update_page_status = Mock()
# Create and select element
element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
widget.selected_elements.add(element)
# Start dragging in resize mode
widget.drag_start_pos = (200, 200)
widget.is_dragging = True
widget.resize_handle = "bottom-right"
widget.resize_start_pos = (100, 100)
widget.resize_start_size = (100, 100)
# Mock _resize_element
widget._resize_element = Mock()
event = Mock()
event.position = Mock(return_value=QPointF(220, 220))
widget.mouseMoveEvent(event)
# Should call _resize_element with deltas
widget._resize_element.assert_called_once()
assert widget.update.called
class TestMultiSelect:
"""Test multi-select functionality"""
def test_ctrl_click_non_image_adds_to_selection(self, qtbot):
"""Test Ctrl+click on non-ImageData adds to multi-selection"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create and add first element
element1 = TextBoxData(
text_content="Test 1",
x=100,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
)
widget.selected_elements.add(element1)
# Create second element
element2 = TextBoxData(
text_content="Test 2",
x=250,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
)
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(300, 125))
event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)
widget._get_element_at = Mock(return_value=element2)
widget._get_page_at = Mock(return_value=(None, -1, None))
widget._check_ghost_page_click = Mock(return_value=False)
widget.mousePressEvent(event)
# Should add to selection
assert element1 in widget.selected_elements
assert element2 in widget.selected_elements
assert len(widget.selected_elements) == 2
def test_ctrl_click_selected_element_removes_from_selection(self, qtbot):
"""Test Ctrl+click on already selected element removes it"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create and select two elements
element1 = TextBoxData(
text_content="Test 1",
x=100,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
)
element2 = TextBoxData(
text_content="Test 2",
x=250,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
)
widget.selected_elements.add(element1)
widget.selected_elements.add(element2)
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(150, 125))
event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)
widget._get_element_at = Mock(return_value=element1)
widget._get_page_at = Mock(return_value=(None, -1, None))
widget._check_ghost_page_click = Mock(return_value=False)
widget.mousePressEvent(event)
# Should remove from selection
assert element1 not in widget.selected_elements
assert element2 in widget.selected_elements
assert len(widget.selected_elements) == 1
def test_shift_click_adds_to_selection(self, qtbot):
"""Test Shift+click adds element to selection"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create and select first element
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
widget.selected_elements.add(element1)
# Create second element
element2 = ImageData(image_path="/test2.jpg", x=250, y=100, width=100, height=100)
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(300, 150))
event.modifiers = Mock(return_value=Qt.KeyboardModifier.ShiftModifier)
widget._get_element_at = Mock(return_value=element2)
widget._get_page_at = Mock(return_value=(None, -1, None))
widget._check_ghost_page_click = Mock(return_value=False)
widget.mousePressEvent(event)
# Should add to selection
assert element1 in widget.selected_elements
assert element2 in widget.selected_elements
assert len(widget.selected_elements) == 2
def test_shift_click_selected_element_removes_it(self, qtbot):
"""Test Shift+click on selected element removes it"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create and select two elements
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
element2 = ImageData(image_path="/test2.jpg", x=250, y=100, width=100, height=100)
widget.selected_elements.add(element1)
widget.selected_elements.add(element2)
event = Mock()
event.button = Mock(return_value=Qt.MouseButton.LeftButton)
event.position = Mock(return_value=QPointF(150, 150))
event.modifiers = Mock(return_value=Qt.KeyboardModifier.ShiftModifier)
widget._get_element_at = Mock(return_value=element1)
widget._get_page_at = Mock(return_value=(None, -1, None))
widget._check_ghost_page_click = Mock(return_value=False)
widget.mousePressEvent(event)
# Should remove from selection
assert element1 not in widget.selected_elements
assert element2 in widget.selected_elements
assert len(widget.selected_elements) == 1
class TestElementPositioningWithoutParentPage:
"""Test element positioning when element has no parent page"""
def test_drag_element_without_parent_page(self, qtbot):
"""Test dragging element that has no _parent_page attribute"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget._update_page_status = Mock()
# Create element without _parent_page
element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
widget.selected_elements.add(element)
# Start dragging
widget.drag_start_pos = (150, 150)
widget.is_dragging = True
widget.drag_start_element_pos = (100, 100)
# Mock page detection to return a page
mock_page = Mock()
mock_renderer = Mock()
widget._get_page_at = Mock(return_value=(mock_page, 0, mock_renderer))
event = Mock()
event.position = Mock(return_value=QPointF(180, 180))
widget.mouseMoveEvent(event)
# Element position should be updated (without snapping since no parent page)
assert element.position == (130, 130) # Moved by 30 pixels / zoom_level (1.0)
assert widget.update.called
class TestWheelEventWhileDragging:
"""Test wheel events during drag operations"""
def test_ctrl_scroll_while_dragging_adjusts_drag_start_pos(self, qtbot):
"""Test Ctrl+scroll while dragging adjusts drag_start_pos for zoom"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget.clamp_pan_offset = Mock()
# Setup dragging state
widget.is_dragging = True
widget.drag_start_pos = (100, 100)
event = Mock()
event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=120))) # Zoom in
event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)
event.position = Mock(return_value=QPointF(150, 150))
old_drag_pos = widget.drag_start_pos
widget.wheelEvent(event)
# drag_start_pos should be adjusted
assert widget.drag_start_pos != old_drag_pos
assert widget.is_dragging is True
def test_scroll_while_dragging_adjusts_drag_start_pos(self, qtbot):
"""Test scrolling while dragging adjusts drag_start_pos for pan"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
widget.clamp_pan_offset = Mock()
# Setup dragging state
widget.is_dragging = True
widget.drag_start_pos = (100, 100)
initial_drag_y = widget.drag_start_pos[1]
event = Mock()
event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), y=Mock(return_value=120))) # Scroll up
event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)
widget.wheelEvent(event)
# drag_start_pos y should be adjusted for pan
assert widget.drag_start_pos[1] != initial_drag_y
assert widget.is_dragging is True
class TestEditTextElement:
"""Test _edit_text_element dialog functionality"""
def test_edit_text_element_accepted(self, qtbot):
"""Test editing text element when dialog is accepted"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create text element
text_element = TextBoxData(
text_content="Original",
x=100,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
alignment="left",
)
# Mock TextEditDialog - patch where it's imported
with patch("pyPhotoAlbum.text_edit_dialog.TextEditDialog") as MockDialog:
# Create mock instance
mock_instance = Mock()
MockDialog.return_value = mock_instance
# Mock DialogCode.Accepted (needed for comparison in code)
mock_dialog_code = Mock()
mock_dialog_code.Accepted = 1
MockDialog.DialogCode = mock_dialog_code
# Set up the dialog to return Accepted (1)
mock_instance.exec.return_value = 1
mock_instance.get_values.return_value = {
"text_content": "Updated",
"font_settings": {"family": "Helvetica", "size": 14, "color": (0, 0, 0)},
"alignment": "center",
}
widget._edit_text_element(text_element)
# Verify dialog was created and methods called
MockDialog.assert_called_once_with(text_element, widget)
mock_instance.exec.assert_called_once()
mock_instance.get_values.assert_called_once()
# Should update element
assert text_element.text_content == "Updated"
assert text_element.font_settings["family"] == "Helvetica"
assert text_element.alignment == "center"
assert widget.update.called
def test_edit_text_element_rejected(self, qtbot):
"""Test editing text element when dialog is rejected"""
widget = TestMouseInteractionWidget()
qtbot.addWidget(widget)
widget.update = Mock()
# Create text element
text_element = TextBoxData(
text_content="Original",
x=100,
y=100,
width=100,
height=50,
font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
alignment="left",
)
original_content = text_element.text_content
# Mock TextEditDialog
with patch("pyPhotoAlbum.text_edit_dialog.TextEditDialog") as MockDialog:
mock_dialog = MockDialog.return_value
mock_dialog.exec = Mock(return_value=0) # Rejected
widget._edit_text_element(text_element)
# Should not update element
assert text_element.text_content == original_content
assert not widget.update.called