""" Tests for ElementOperationsMixin """ import pytest from unittest.mock import Mock, MagicMock, patch, mock_open from PyQt6.QtWidgets import QMainWindow, QFileDialog from pyPhotoAlbum.mixins.operations.element_ops import ElementOperationsMixin from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.commands import CommandHistory from PIL import Image import io # Create test window with ElementOperationsMixin class TestElementWindow(ElementOperationsMixin, QMainWindow): """Test window with element operations mixin""" def __init__(self): super().__init__() # Mock GL widget self.gl_widget = Mock() # Mock project self.project = Mock() self.project.history = CommandHistory() self.project.asset_manager = Mock() # Track method calls self._update_view_called = False self._status_message = None self._error_message = None self._require_page_called = False self._current_page_index = 0 def require_page(self): """Track require_page calls""" self._require_page_called = True return self._current_page is not None if hasattr(self, '_current_page') else False def get_current_page(self): """Return mock current page""" if hasattr(self, '_current_page'): return self._current_page return None def get_current_page_index(self): """Return current page index""" return self._current_page_index def update_view(self): """Track update_view calls""" self._update_view_called = True def show_status(self, message, timeout=0): """Track status messages""" self._status_message = message def show_error(self, title, message): """Track error messages""" self._error_message = message class TestAddImage: """Test add_image method""" @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') @patch('pyPhotoAlbum.mixins.operations.element_ops.Image.open') def test_add_image_success(self, mock_image_open, mock_file_dialog, qtbot): """Test successfully adding an image""" window = TestElementWindow() qtbot.addWidget(window) # Setup page layout = PageLayout() layout.size = (210, 297) # A4 size page = Mock() page.layout = layout window._current_page = page # Mock file dialog mock_file_dialog.return_value = ("/path/to/image.jpg", "Image Files (*.jpg)") # Mock PIL Image mock_img = Mock() mock_img.size = (800, 600) mock_image_open.return_value = mock_img # Mock asset manager window.project.asset_manager.import_asset.return_value = "assets/image.jpg" window.add_image() # Should have called asset manager assert window.project.asset_manager.import_asset.called # Should have created command assert window.project.history.can_undo() # Should update view assert window._update_view_called assert "added image" in window._status_message.lower() @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') def test_add_image_cancelled(self, mock_file_dialog, qtbot): """Test cancelling image selection""" window = TestElementWindow() qtbot.addWidget(window) layout = PageLayout() layout.size = (210, 297) page = Mock() page.layout = layout window._current_page = page # Mock file dialog returning empty (cancelled) mock_file_dialog.return_value = ("", "") window.add_image() # Should not add anything assert not window._update_view_called def test_add_image_no_page(self, qtbot): """Test adding image with no current page""" window = TestElementWindow() qtbot.addWidget(window) window._current_page = None window.add_image() # Should check for page and return early assert window._require_page_called assert not window._update_view_called @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') @patch('pyPhotoAlbum.mixins.operations.element_ops.Image.open') def test_add_image_scales_large_image(self, mock_image_open, mock_file_dialog, qtbot): """Test that large images are scaled down""" window = TestElementWindow() qtbot.addWidget(window) layout = PageLayout() layout.size = (210, 297) page = Mock() page.layout = layout window._current_page = page mock_file_dialog.return_value = ("/path/to/large.jpg", "Image Files (*.jpg)") # Mock very large image mock_img = Mock() mock_img.size = (3000, 2000) # Much larger than max_size=300 mock_image_open.return_value = mock_img window.project.asset_manager.import_asset.return_value = "assets/large.jpg" window.add_image() # Image should be added (scaled down internally) assert window._update_view_called @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') @patch('pyPhotoAlbum.mixins.operations.element_ops.Image.open') def test_add_image_error_handling(self, mock_image_open, mock_file_dialog, qtbot): """Test error handling when adding image fails""" window = TestElementWindow() qtbot.addWidget(window) layout = PageLayout() layout.size = (210, 297) page = Mock() page.layout = layout window._current_page = page mock_file_dialog.return_value = ("/path/to/broken.jpg", "Image Files (*.jpg)") # Mock error mock_image_open.side_effect = Exception("Cannot open image") window.add_image() # Should show error assert window._error_message is not None assert "failed to add image" in window._error_message.lower() class TestAddText: """Test add_text method""" def test_add_text_success(self, qtbot): """Test successfully adding a text box""" window = TestElementWindow() qtbot.addWidget(window) # Setup page layout = PageLayout() layout.size = (210, 297) # A4 size page = Mock() page.layout = layout window._current_page = page # Mock layout.add_element layout.add_element = Mock() window.add_text() # Should have added text element assert layout.add_element.called args = layout.add_element.call_args[0] text_element = args[0] assert isinstance(text_element, TextBoxData) assert text_element.text_content == "New Text" assert text_element.size == (200, 50) # Should be centered expected_x = (210 - 200) / 2 expected_y = (297 - 50) / 2 assert text_element.position == (expected_x, expected_y) assert window._update_view_called def test_add_text_no_page(self, qtbot): """Test adding text with no current page""" window = TestElementWindow() qtbot.addWidget(window) window._current_page = None window.add_text() # Should check for page and return early assert window._require_page_called assert not window._update_view_called class TestAddPlaceholder: """Test add_placeholder method""" def test_add_placeholder_success(self, qtbot): """Test successfully adding a placeholder""" window = TestElementWindow() qtbot.addWidget(window) # Setup page layout = PageLayout() layout.size = (210, 297) page = Mock() page.layout = layout window._current_page = page # Mock layout.add_element layout.add_element = Mock() window.add_placeholder() # Should have added placeholder element assert layout.add_element.called args = layout.add_element.call_args[0] placeholder_element = args[0] assert isinstance(placeholder_element, PlaceholderData) assert placeholder_element.placeholder_type == "image" assert placeholder_element.size == (200, 150) # Should be centered expected_x = (210 - 200) / 2 expected_y = (297 - 150) / 2 assert placeholder_element.position == (expected_x, expected_y) assert window._update_view_called def test_add_placeholder_no_page(self, qtbot): """Test adding placeholder with no current page""" window = TestElementWindow() qtbot.addWidget(window) window._current_page = None window.add_placeholder() # Should check for page and return early assert window._require_page_called assert not window._update_view_called class TestElementOperationsIntegration: """Test integration between element operations""" @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') @patch('pyPhotoAlbum.mixins.operations.element_ops.Image.open') def test_add_multiple_elements(self, mock_image_open, mock_file_dialog, qtbot): """Test adding multiple different element types""" window = TestElementWindow() qtbot.addWidget(window) layout = PageLayout() layout.size = (210, 297) layout.add_element = Mock() page = Mock() page.layout = layout window._current_page = page # Add text window.add_text() assert layout.add_element.call_count == 1 # Add placeholder window.add_placeholder() assert layout.add_element.call_count == 2 # Add image mock_file_dialog.return_value = ("/test.jpg", "Image Files") mock_img = Mock() mock_img.size = (100, 100) mock_image_open.return_value = mock_img window.project.asset_manager.import_asset.return_value = "assets/test.jpg" window.add_image() # Should have added all three elements assert window._update_view_called @patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName') @patch('pyPhotoAlbum.mixins.operations.element_ops.Image.open') def test_add_image_with_undo(self, mock_image_open, mock_file_dialog, qtbot): """Test that adding image can be undone""" window = TestElementWindow() qtbot.addWidget(window) layout = PageLayout() layout.size = (210, 297) page = Mock() page.layout = layout window._current_page = page mock_file_dialog.return_value = ("/test.jpg", "Image Files") mock_img = Mock() mock_img.size = (200, 150) mock_image_open.return_value = mock_img window.project.asset_manager.import_asset.return_value = "assets/test.jpg" # Should have no commands initially assert not window.project.history.can_undo() window.add_image() # Should have created a command assert window.project.history.can_undo() # Can undo initial_count = len(layout.elements) window.project.history.undo() assert len(layout.elements) < initial_count or layout.elements == [] # Can redo window.project.history.redo() assert len(layout.elements) >= initial_count