pyPhotoAlbum/tests/test_element_ops_mixin.py
Duncan Tourolle fae9e5bd2b
Some checks failed
Python CI / test (push) Successful in 1m17s
Lint / lint (push) Successful in 1m32s
Tests / test (3.10) (push) Successful in 1m10s
Tests / test (3.9) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Additional refactoring
2025-11-27 21:57:57 +01:00

362 lines
12 KiB
Python
Executable File

"""
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.mixins.asset_path import AssetPathMixin
from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.commands import CommandHistory
# Create test window with ElementOperationsMixin
class TestElementWindow(ElementOperationsMixin, AssetPathMixin, 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()
self._project.folder_path = "/tmp/test_project"
# 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
@property
def project(self):
return self._project
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.get_image_dimensions')
def test_add_image_success(self, mock_get_dims, 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 get_image_dimensions (returns scaled dimensions)
mock_get_dims.return_value = (300, 225) # 800x600 scaled to max 300
# 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.get_image_dimensions')
def test_add_image_scales_large_image(self, mock_get_dims, 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 get_image_dimensions returning scaled dimensions (3000x2000 -> 300x200)
mock_get_dims.return_value = (300, 200)
window.project.asset_manager.import_asset.return_value = "assets/large.jpg"
window.add_image()
# Image should be added (scaled down by get_image_dimensions)
assert window._update_view_called
@patch('pyPhotoAlbum.mixins.operations.element_ops.QFileDialog.getOpenFileName')
@patch('pyPhotoAlbum.mixins.operations.element_ops.get_image_dimensions')
def test_add_image_fallback_dimensions(self, mock_get_dims, mock_file_dialog, qtbot):
"""Test fallback dimensions when get_image_dimensions returns None"""
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 get_image_dimensions returning None (image unreadable)
mock_get_dims.return_value = None
window.project.asset_manager.import_asset.return_value = "assets/broken.jpg"
window.add_image()
# Should still add image with fallback dimensions (200x150)
assert window._update_view_called
assert window.project.history.can_undo()
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.get_image_dimensions')
def test_add_multiple_elements(self, mock_get_dims, 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_get_dims.return_value = (100, 100)
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.get_image_dimensions')
def test_add_image_with_undo(self, mock_get_dims, 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_get_dims.return_value = (200, 150)
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