364 lines
11 KiB
Python
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
|