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
460 lines
17 KiB
Python
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
|