""" Tests for TemplateOperationsMixin """ import pytest from unittest.mock import Mock, MagicMock, patch from PyQt6.QtWidgets import QMainWindow, QDialog from pyPhotoAlbum.mixins.base import ApplicationStateMixin from pyPhotoAlbum.mixins.operations.template_ops import TemplateOperationsMixin from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.models import ImageData from pyPhotoAlbum.commands import CommandHistory class TestTemplateOpsWindow(TemplateOperationsMixin, ApplicationStateMixin, QMainWindow): """Test window with template operations mixin""" def __init__(self): super().__init__() self._gl_widget = Mock() self._gl_widget.current_page_index = 0 self._gl_widget.zoom_level = 1.0 self._gl_widget.pan_offset = [0, 0] self._gl_widget._page_renderers = [] self._gl_widget.width = Mock(return_value=800) self._gl_widget.height = Mock(return_value=600) self._project = Project(name="Test") self._project.page_size_mm = (210, 297) self._project.working_dpi = 96 self._project.history = CommandHistory() self._template_manager = Mock() self._update_view_called = False self._status_message = None self._info_title = None self._info_message = None self._warning_title = None self._warning_message = None self._error_title = None self._error_message = None def update_view(self): self._update_view_called = True def show_status(self, message, timeout=0): self._status_message = message def show_info(self, title, message): self._info_title = title self._info_message = message def show_warning(self, title, message): self._warning_title = title self._warning_message = message def show_error(self, title, message): self._error_title = title self._error_message = message class TestSavePageAsTemplate: """Test save_page_as_template method""" def test_save_template_no_current_page(self, qtbot): """Test returns early when no current page""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.project.pages = [] window.save_page_as_template() # Should return early without showing dialogs assert not window._update_view_called def test_save_template_empty_page(self, qtbot): """Test shows warning when page has no elements""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create empty page layout = PageLayout(width=210, height=297) page = Page(layout=layout, page_number=1) window.project.pages = [page] window.save_page_as_template() # Should show warning about empty page assert window._warning_title == "Empty Page" assert "Cannot save an empty page" in window._warning_message @patch("pyPhotoAlbum.mixins.operations.template_ops.QInputDialog.getText") def test_save_template_user_cancels_name(self, mock_get_text, qtbot): """Test returns when user cancels name dialog""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page with elements layout = PageLayout(width=210, height=297) layout.elements = [ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)] page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = [] # Mock user canceling the name dialog mock_get_text.return_value = ("", False) window.save_page_as_template() # Should return without saving assert not window.template_manager.save_template.called @patch("pyPhotoAlbum.mixins.operations.template_ops.QInputDialog.getText") def test_save_template_user_cancels_description(self, mock_get_text, qtbot): """Test continues with empty description when user cancels description dialog""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page with elements layout = PageLayout(width=210, height=297) layout.elements = [ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)] page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = [] # Mock template creation mock_template = Mock() window.template_manager.create_template_from_page.return_value = mock_template # Mock dialogs: name OK, description canceled mock_get_text.side_effect = [ ("My Template", True), # Name dialog ("Some description", False) # Description dialog canceled ] window.save_page_as_template() # Should still save with empty description window.template_manager.create_template_from_page.assert_called_once() call_args = window.template_manager.create_template_from_page.call_args assert call_args[0][0] == page assert call_args[0][1] == "My Template" assert call_args[0][2] == "" # Empty description window.template_manager.save_template.assert_called_once_with(mock_template) assert window._info_title == "Template Saved" @patch("pyPhotoAlbum.mixins.operations.template_ops.QInputDialog.getText") def test_save_template_success(self, mock_get_text, qtbot): """Test successfully saving a template""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page with elements layout = PageLayout(width=210, height=297) layout.elements = [ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)] page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = ["Template_1", "Template_2"] # Mock template creation mock_template = Mock() window.template_manager.create_template_from_page.return_value = mock_template # Mock dialogs mock_get_text.side_effect = [ ("My Template", True), # Name dialog ("A description", True) # Description dialog ] window.save_page_as_template() # Verify template creation with correct parameters window.template_manager.create_template_from_page.assert_called_once_with( page, "My Template", "A description" ) # Verify template was saved window.template_manager.save_template.assert_called_once_with(mock_template) # Verify success message assert window._info_title == "Template Saved" assert "My Template" in window._info_message @patch("pyPhotoAlbum.mixins.operations.template_ops.QInputDialog.getText") def test_save_template_default_name_numbering(self, mock_get_text, qtbot): """Test default template name includes correct numbering""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page with elements layout = PageLayout(width=210, height=297) layout.elements = [ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)] page = Page(layout=layout, page_number=1) window.project.pages = [page] # Mock 3 existing templates window.template_manager.list_templates.return_value = ["Template_1", "Template_2", "Template_3"] mock_get_text.return_value = ("", False) # User cancels window.save_page_as_template() # Check that the default name offered was "Template_4" call_args = mock_get_text.call_args assert call_args[1]["text"] == "Template_4" @patch("pyPhotoAlbum.mixins.operations.template_ops.QInputDialog.getText") def test_save_template_exception_handling(self, mock_get_text, qtbot): """Test handles exceptions during template save""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page with elements layout = PageLayout(width=210, height=297) layout.elements = [ImageData(image_path="test.jpg", x=0, y=0, width=100, height=100)] page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = [] # Mock dialogs mock_get_text.side_effect = [ ("My Template", True), ("Description", True) ] # Mock template creation to raise exception window.template_manager.create_template_from_page.side_effect = Exception("Template error") window.save_page_as_template() # Should show error message assert window._error_title == "Error" assert "Failed to save template" in window._error_message assert "Template error" in window._error_message class TestNewPageFromTemplate: """Test new_page_from_template method""" def test_new_page_no_templates(self, qtbot): """Test shows info when no templates available""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.template_manager.list_templates.return_value = [] window.new_page_from_template() assert window._info_title == "No Templates" assert "No templates available" in window._info_message def test_new_page_user_cancels_dialog(self, qtbot): """Test returns when user cancels dialog""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.template_manager.list_templates.return_value = ["Template_1"] # Patch QDialog.exec to return rejected with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Rejected): window.new_page_from_template() # Should not create page assert not window.template_manager.load_template.called def _mock_dialog_exec(self, template_name="Template_1", scale_id=1, margin=2.5): """Helper to mock dialog exec with specific values""" original_exec = QDialog.exec def mock_exec(self): # Find the widgets that were added to the dialog combo = self.findChild(Mock.__class__.__bases__[0], "") # This won't work, need different approach # Set values on widgets before returning return QDialog.DialogCode.Accepted return mock_exec def test_new_page_stretch_mode(self, qtbot): """Test creates page with stretch scaling mode""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.template_manager.list_templates.return_value = ["Template_1"] # Create initial page layout = PageLayout(width=210, height=297) page1 = Page(layout=layout, page_number=1) window.project.pages = [page1] # Mock template mock_template = Mock() window.template_manager.load_template.return_value = mock_template # Mock new page mock_new_page = Mock() window.template_manager.create_page_from_template.return_value = mock_new_page # Capture the dialog and manipulate it captured_dialog = None original_exec = QDialog.exec def mock_exec(dialog_self): nonlocal captured_dialog captured_dialog = dialog_self # Find and set widget values for child in dialog_self.children(): # Template combo - first one should be selected by default (index 0) if hasattr(child, 'currentText'): pass # Already set to first item # Button groups - stretch radio should already be checked (it's default) # Margin spinbox - already set to 2.5 (default) return QDialog.DialogCode.Accepted with patch.object(QDialog, "exec", mock_exec): window.new_page_from_template() # Verify template was loaded window.template_manager.load_template.assert_called_once_with("Template_1") # Verify page creation with correct parameters (defaults) window.template_manager.create_page_from_template.assert_called_once() call_args = window.template_manager.create_page_from_template.call_args assert call_args[1]["page_number"] == 2 assert call_args[1]["target_size_mm"] == (210, 297) assert call_args[1]["scale_mode"] == "stretch" # Default checked assert call_args[1]["margin_percent"] == 2.5 # Default value # Verify page was added to project assert len(window.project.pages) == 2 assert window._update_view_called def test_new_page_exception_handling(self, qtbot): """Test handles exceptions during page creation""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.template_manager.list_templates.return_value = ["Template_1"] window.project.pages = [] # Mock template loading to raise exception window.template_manager.load_template.side_effect = Exception("Template load error") with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Accepted): window.new_page_from_template() # Should show error message assert window._error_title == "Error" assert "Failed to create page from template" in window._error_message assert "Template load error" in window._error_message class TestApplyTemplateToPage: """Test apply_template_to_page method""" def test_apply_template_no_current_page(self, qtbot): """Test returns early when no current page""" window = TestTemplateOpsWindow() qtbot.addWidget(window) window.project.pages = [] window.apply_template_to_page() # Should return early without showing dialogs assert not window._update_view_called def test_apply_template_no_templates(self, qtbot): """Test shows info when no templates available""" window = TestTemplateOpsWindow() qtbot.addWidget(window) # Create page layout = PageLayout(width=210, height=297) page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = [] window.apply_template_to_page() assert window._info_title == "No Templates" assert "No templates available" in window._info_message def test_apply_template_user_cancels_dialog(self, qtbot): """Test returns when user cancels dialog""" window = TestTemplateOpsWindow() qtbot.addWidget(window) layout = PageLayout(width=210, height=297) page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = ["Template_1"] # Mock dialog rejection with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Rejected): window.apply_template_to_page() # Should not apply template assert not window.template_manager.load_template.called assert not window._update_view_called def test_apply_template_replace_mode(self, qtbot): """Test applies template in replace mode""" window = TestTemplateOpsWindow() qtbot.addWidget(window) layout = PageLayout(width=210, height=297) page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = ["Template_1"] # Mock template mock_template = Mock() window.template_manager.load_template.return_value = mock_template # Use default values: replace mode (checked by default), stretch mode (checked by default), margin=2.5 with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Accepted): window.apply_template_to_page() # Verify template application with default values window.template_manager.apply_template_to_page.assert_called_once() call_args = window.template_manager.apply_template_to_page.call_args assert call_args[0][0] == mock_template assert call_args[0][1] == page assert call_args[1]["mode"] == "replace" # Default assert call_args[1]["scale_mode"] == "stretch" # Default assert call_args[1]["margin_percent"] == 2.5 # Default assert window._update_view_called assert "Template_1" in window._status_message def test_apply_template_exception_handling(self, qtbot): """Test handles exceptions during template application""" window = TestTemplateOpsWindow() qtbot.addWidget(window) layout = PageLayout(width=210, height=297) page = Page(layout=layout, page_number=1) window.project.pages = [page] window.template_manager.list_templates.return_value = ["Template_1"] # Mock template loading to raise exception window.template_manager.load_template.side_effect = Exception("Template error") with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Accepted): window.apply_template_to_page() # Should show error message assert window._error_title == "Error" assert "Failed to apply template" in window._error_message assert "Template error" in window._error_message