""" 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