All checks were successful
Python CI / test (push) Successful in 1m28s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m41s
Tests / test (3.12) (push) Successful in 1m42s
Tests / test (3.13) (push) Successful in 1m35s
Tests / test (3.14) (push) Successful in 1m15s
986 lines
34 KiB
Python
Executable File
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(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(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(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(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
|