pyPhotoAlbum/tests/test_element_selection_mixin.py
Duncan Tourolle 7f32858baf
All checks were successful
Python CI / test (push) Successful in 1m7s
Lint / lint (push) Successful in 1m11s
Tests / test (3.10) (push) Successful in 50s
Tests / test (3.11) (push) Successful in 51s
Tests / test (3.9) (push) Successful in 47s
big refactor to use mixin architecture
2025-11-11 10:35:24 +01:00

399 lines
15 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
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
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