pyPhotoAlbum/tests/test_template_ops_mixin.py
Duncan Tourolle b18a780a33
All checks were successful
Python CI / test (push) Successful in 1m28s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m41s
Tests / test (3.12) (push) Successful in 1m42s
Tests / test (3.13) (push) Successful in 1m35s
Tests / test (3.14) (push) Successful in 1m15s
increase test coverage
2025-11-28 19:54:41 +01:00

460 lines
17 KiB
Python

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