486 lines
19 KiB
Python
486 lines
19 KiB
Python
"""
|
|
Tests for ElementSelectionMixin
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
from PyQt6.QtWidgets import QApplication
|
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
|
|
from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData
|
|
from pyPhotoAlbum.project import Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_page_renderer():
|
|
"""Create a mock PageRenderer"""
|
|
renderer = Mock()
|
|
renderer.screen_x = 50
|
|
renderer.screen_y = 50
|
|
renderer.zoom = 1.0
|
|
renderer.dpi = 96
|
|
|
|
# Mock coordinate conversion methods
|
|
def page_to_screen(x, y):
|
|
return (renderer.screen_x + x * renderer.zoom,
|
|
renderer.screen_y + y * renderer.zoom)
|
|
|
|
def screen_to_page(x, y):
|
|
return ((x - renderer.screen_x) / renderer.zoom,
|
|
(y - renderer.screen_y) / renderer.zoom)
|
|
|
|
def is_point_in_page(x, y):
|
|
# Simple bounds check (assume 210mm x 297mm page at 96 DPI)
|
|
page_width_px = 210 * 96 / 25.4
|
|
page_height_px = 297 * 96 / 25.4
|
|
return (renderer.screen_x <= x <= renderer.screen_x + page_width_px * renderer.zoom and
|
|
renderer.screen_y <= y <= renderer.screen_y + page_height_px * renderer.zoom)
|
|
|
|
renderer.page_to_screen = page_to_screen
|
|
renderer.screen_to_page = screen_to_page
|
|
renderer.is_point_in_page = is_point_in_page
|
|
|
|
return renderer
|
|
|
|
|
|
# Create a minimal test widget class
|
|
class TestSelectionWidget(ElementSelectionMixin, QOpenGLWidget):
|
|
"""Test widget combining ElementSelectionMixin with QOpenGLWidget"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._page_renderers = []
|
|
|
|
|
|
class TestElementSelectionInitialization:
|
|
"""Test ElementSelectionMixin initialization"""
|
|
|
|
def test_initialization_creates_empty_selection_set(self, qtbot):
|
|
"""Test that mixin initializes with empty selection set"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
assert hasattr(widget, 'selected_elements')
|
|
assert isinstance(widget.selected_elements, set)
|
|
assert len(widget.selected_elements) == 0
|
|
|
|
def test_selected_element_property_returns_none_when_empty(self, qtbot):
|
|
"""Test that selected_element property returns None when no selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
assert widget.selected_element is None
|
|
|
|
def test_selected_element_property_returns_first_when_populated(self, qtbot):
|
|
"""Test that selected_element property returns first element"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem1 = ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)
|
|
elem2 = PlaceholderData(x=50, y=50, width=80, height=80)
|
|
|
|
widget.selected_elements = {elem1, elem2}
|
|
|
|
# Should return one of them (sets are unordered, but there should be exactly one)
|
|
result = widget.selected_element
|
|
assert result is not None
|
|
assert result in {elem1, elem2}
|
|
|
|
|
|
class TestElementSelectionProperty:
|
|
"""Test selected_element property setter/getter"""
|
|
|
|
def test_set_selected_element_to_single_element(self, qtbot):
|
|
"""Test setting selected_element with single element"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)
|
|
widget.selected_element = elem
|
|
|
|
assert len(widget.selected_elements) == 1
|
|
assert elem in widget.selected_elements
|
|
assert widget.selected_element == elem
|
|
|
|
def test_set_selected_element_to_none_clears_selection(self, qtbot):
|
|
"""Test setting selected_element to None clears selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)
|
|
widget.selected_element = elem
|
|
|
|
widget.selected_element = None
|
|
|
|
assert len(widget.selected_elements) == 0
|
|
assert widget.selected_element is None
|
|
|
|
def test_set_selected_element_replaces_previous(self, qtbot):
|
|
"""Test setting selected_element replaces previous selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem1 = ImageData(image_path="test1.jpg", x=0, y=0, width=100, height=100)
|
|
elem2 = PlaceholderData(x=50, y=50, width=80, height=80)
|
|
|
|
widget.selected_element = elem1
|
|
assert widget.selected_element == elem1
|
|
|
|
widget.selected_element = elem2
|
|
assert widget.selected_element == elem2
|
|
assert len(widget.selected_elements) == 1
|
|
assert elem1 not in widget.selected_elements
|
|
|
|
|
|
class TestGetElementAt:
|
|
"""Test _get_element_at method"""
|
|
|
|
def test_get_element_at_no_renderers(self, qtbot):
|
|
"""Test _get_element_at returns None when no renderers"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
widget._page_renderers = []
|
|
|
|
result = widget._get_element_at(100, 100)
|
|
assert result is None
|
|
|
|
def test_get_element_at_outside_page(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at returns None when click is outside page"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click way outside page bounds
|
|
result = widget._get_element_at(5000, 5000)
|
|
assert result is None
|
|
|
|
def test_get_element_at_finds_element(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at finds element at position"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
page.layout.add_element(elem)
|
|
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click in middle of element (screen coords: 50 + 150 = 200, 50 + 175 = 225)
|
|
result = widget._get_element_at(200, 225)
|
|
|
|
assert result is not None
|
|
assert result == elem
|
|
assert hasattr(result, '_page_renderer')
|
|
assert hasattr(result, '_parent_page')
|
|
|
|
def test_get_element_at_finds_topmost_element(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at returns topmost element when overlapping"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
|
|
# Add overlapping elements (higher z-index = on top)
|
|
elem1 = ImageData(image_path="bottom.jpg", x=100, y=100, width=200, height=200, z_index=0)
|
|
elem2 = PlaceholderData(x=150, y=150, width=100, height=100, z_index=1)
|
|
|
|
page.layout.add_element(elem1)
|
|
page.layout.add_element(elem2)
|
|
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click in overlapping region (screen: 50 + 175 = 225, 50 + 175 = 225)
|
|
result = widget._get_element_at(225, 225)
|
|
|
|
# Should return elem2 (topmost - last in list)
|
|
assert result == elem2
|
|
|
|
def test_get_element_at_handles_empty_page(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at returns None for empty page"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click inside page but no elements
|
|
result = widget._get_element_at(200, 200)
|
|
assert result is None
|
|
|
|
def test_get_element_at_element_at_edge(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at detects element at exact edge"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
page.layout.add_element(elem)
|
|
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click exactly at element edge (screen: 50 + 100 = 150, 50 + 100 = 150)
|
|
result = widget._get_element_at(150, 150)
|
|
assert result == elem
|
|
|
|
# Click just outside element (screen: 50 + 301 = 351, 50 + 251 = 301)
|
|
result = widget._get_element_at(351, 301)
|
|
assert result is None
|
|
|
|
def test_get_element_at_rotated_element(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at handles rotated elements correctly"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
# Create element rotated 45 degrees
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.rotation = 45
|
|
page.layout.add_element(elem)
|
|
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click at center of rotated element (should still be inside)
|
|
# Center is at (100 + 200/2, 100 + 150/2) = (200, 175) in page coords
|
|
# Screen coords: (50 + 200, 50 + 175) = (250, 225)
|
|
result = widget._get_element_at(250, 225)
|
|
assert result == elem
|
|
assert hasattr(result, '_page_renderer')
|
|
assert hasattr(result, '_parent_page')
|
|
|
|
def test_get_element_at_rotated_element_outside(self, qtbot, mock_page_renderer):
|
|
"""Test _get_element_at correctly rejects clicks outside rotated element"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
# Create element rotated 90 degrees
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.rotation = 90
|
|
page.layout.add_element(elem)
|
|
|
|
widget._page_renderers = [(mock_page_renderer, page)]
|
|
|
|
# Click far outside element
|
|
result = widget._get_element_at(500, 500)
|
|
assert result is None
|
|
|
|
|
|
class TestGetResizeHandleAt:
|
|
"""Test _get_resize_handle_at method"""
|
|
|
|
def test_get_resize_handle_no_selection(self, qtbot):
|
|
"""Test _get_resize_handle_at returns None when no selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
result = widget._get_resize_handle_at(100, 100)
|
|
assert result is None
|
|
|
|
def test_get_resize_handle_no_project(self, qtbot):
|
|
"""Test _get_resize_handle_at returns None when no project"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
|
|
# Mock window without project
|
|
mock_window = Mock()
|
|
mock_window.project = None
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
result = widget._get_resize_handle_at(100, 100)
|
|
assert result is None
|
|
|
|
def test_get_resize_handle_no_renderer(self, qtbot):
|
|
"""Test _get_resize_handle_at returns None when element has no renderer"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
widget.selected_element = elem
|
|
|
|
# Mock window with project
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
result = widget._get_resize_handle_at(100, 100)
|
|
assert result is None
|
|
|
|
def test_get_resize_handle_detects_nw_corner(self, qtbot, mock_page_renderer):
|
|
"""Test _get_resize_handle_at detects northwest corner"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem._page_renderer = mock_page_renderer
|
|
widget.selected_element = elem
|
|
|
|
# Mock window with project
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# Click on NW handle (screen: 50 + 100 = 150, 50 + 100 = 150)
|
|
result = widget._get_resize_handle_at(150, 150)
|
|
assert result == 'nw'
|
|
|
|
def test_get_resize_handle_detects_all_corners(self, qtbot, mock_page_renderer):
|
|
"""Test _get_resize_handle_at detects all four corners"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem._page_renderer = mock_page_renderer
|
|
widget.selected_element = elem
|
|
|
|
# Mock window
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# NW corner (screen: 50 + 100 = 150, 50 + 100 = 150)
|
|
assert widget._get_resize_handle_at(150, 150) == 'nw'
|
|
|
|
# NE corner (screen: 50 + 300 = 350, 50 + 100 = 150)
|
|
assert widget._get_resize_handle_at(350, 150) == 'ne'
|
|
|
|
# SW corner (screen: 50 + 100 = 150, 50 + 250 = 300)
|
|
assert widget._get_resize_handle_at(150, 300) == 'sw'
|
|
|
|
# SE corner (screen: 50 + 300 = 350, 50 + 250 = 300)
|
|
assert widget._get_resize_handle_at(350, 300) == 'se'
|
|
|
|
def test_get_resize_handle_returns_none_for_center(self, qtbot, mock_page_renderer):
|
|
"""Test _get_resize_handle_at returns None for element center"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem._page_renderer = mock_page_renderer
|
|
widget.selected_element = elem
|
|
|
|
# Mock window
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# Click in center of element (screen: 50 + 200 = 250, 50 + 175 = 225)
|
|
result = widget._get_resize_handle_at(250, 225)
|
|
assert result is None
|
|
|
|
def test_get_resize_handle_rotated_element(self, qtbot, mock_page_renderer):
|
|
"""Test _get_resize_handle_at handles rotated elements"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.rotation = 45
|
|
elem._page_renderer = mock_page_renderer
|
|
widget.selected_element = elem
|
|
|
|
# Mock window
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# The rotation code should still detect handles - test NW handle
|
|
# For rotated element, the handle positions are transformed
|
|
result = widget._get_resize_handle_at(150, 150)
|
|
# Should detect a handle (exact handle depends on rotation transform)
|
|
assert result is None or result in ['nw', 'ne', 'sw', 'se']
|
|
|
|
def test_get_resize_handle_rotated_90_degrees(self, qtbot, mock_page_renderer):
|
|
"""Test _get_resize_handle_at handles 90-degree rotated elements"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.rotation = 90
|
|
elem._page_renderer = mock_page_renderer
|
|
widget.selected_element = elem
|
|
|
|
# Mock window
|
|
from pyPhotoAlbum.project import Project, Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
mock_window = Mock()
|
|
mock_window.project = Project(name="Test")
|
|
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
|
|
mock_window.project.pages = [page]
|
|
widget.window = Mock(return_value=mock_window)
|
|
|
|
# Test clicking at various positions - rotation code should handle them
|
|
# Just verify the method runs without crashing
|
|
result = widget._get_resize_handle_at(200, 200)
|
|
assert result is None or result in ['nw', 'ne', 'sw', 'se']
|
|
|
|
|
|
class TestMultiSelect:
|
|
"""Test multi-selection functionality"""
|
|
|
|
def test_multi_select_add_elements(self, qtbot):
|
|
"""Test adding multiple elements to selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem1 = ImageData(image_path="test1.jpg", x=0, y=0, width=100, height=100)
|
|
elem2 = PlaceholderData(x=50, y=50, width=80, height=80)
|
|
|
|
widget.selected_elements.add(elem1)
|
|
widget.selected_elements.add(elem2)
|
|
|
|
assert len(widget.selected_elements) == 2
|
|
assert elem1 in widget.selected_elements
|
|
assert elem2 in widget.selected_elements
|
|
|
|
def test_multi_select_remove_element(self, qtbot):
|
|
"""Test removing element from multi-selection"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem1 = ImageData(image_path="test1.jpg", x=0, y=0, width=100, height=100)
|
|
elem2 = PlaceholderData(x=50, y=50, width=80, height=80)
|
|
|
|
widget.selected_elements = {elem1, elem2}
|
|
widget.selected_elements.remove(elem1)
|
|
|
|
assert len(widget.selected_elements) == 1
|
|
assert elem2 in widget.selected_elements
|
|
assert elem1 not in widget.selected_elements
|
|
|
|
def test_multi_select_clear_all(self, qtbot):
|
|
"""Test clearing all selections"""
|
|
widget = TestSelectionWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem1 = ImageData(image_path="test1.jpg", x=0, y=0, width=100, height=100)
|
|
elem2 = PlaceholderData(x=50, y=50, width=80, height=80)
|
|
|
|
widget.selected_elements = {elem1, elem2}
|
|
widget.selected_elements.clear()
|
|
|
|
assert len(widget.selected_elements) == 0
|