pyPhotoAlbum/tests/test_element_ops_mixin.py
Duncan Tourolle ca21f3ae4c
All checks were successful
Python CI / test (push) Successful in 1m6s
Lint / lint (push) Successful in 1m10s
Tests / test (3.10) (push) Successful in 53s
Tests / test (3.11) (push) Successful in 52s
Tests / test (3.9) (push) Successful in 50s
more tests and gnoem+fedora installer
2025-11-11 12:51:15 +01:00

364 lines
11 KiB
Python

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