All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
332 lines
10 KiB
Python
332 lines
10 KiB
Python
"""
|
|
Integration tests for the refactored UndoableInteractionMixin.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, MagicMock, patch
|
|
from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin
|
|
from pyPhotoAlbum.models import BaseLayoutElement
|
|
|
|
|
|
class MockWidget(UndoableInteractionMixin):
|
|
"""Mock widget that uses the UndoableInteractionMixin."""
|
|
|
|
def __init__(self):
|
|
# Simulate QWidget initialization
|
|
self._mock_window = Mock()
|
|
self._mock_window.project = Mock()
|
|
self._mock_window.project.history = Mock()
|
|
|
|
super().__init__()
|
|
|
|
def window(self):
|
|
"""Mock window() method."""
|
|
return self._mock_window
|
|
|
|
|
|
class TestUndoableInteractionMixinRefactored:
|
|
"""Tests for refactored UndoableInteractionMixin."""
|
|
|
|
def test_initialization(self):
|
|
"""Test that mixin initializes correctly."""
|
|
widget = MockWidget()
|
|
|
|
assert hasattr(widget, "_command_factory")
|
|
assert hasattr(widget, "_interaction_state")
|
|
|
|
def test_begin_move(self):
|
|
"""Test beginning a move interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
|
|
assert widget._interaction_state.element == element
|
|
assert widget._interaction_state.interaction_type == "move"
|
|
assert widget._interaction_state.position == (0.0, 0.0)
|
|
|
|
def test_begin_resize(self):
|
|
"""Test beginning a resize interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
element.size = (100.0, 100.0)
|
|
|
|
widget._begin_resize(element)
|
|
|
|
assert widget._interaction_state.element == element
|
|
assert widget._interaction_state.interaction_type == "resize"
|
|
assert widget._interaction_state.position == (0.0, 0.0)
|
|
assert widget._interaction_state.size == (100.0, 100.0)
|
|
|
|
def test_begin_rotate(self):
|
|
"""Test beginning a rotate interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.rotation = 0.0
|
|
|
|
widget._begin_rotate(element)
|
|
|
|
assert widget._interaction_state.element == element
|
|
assert widget._interaction_state.interaction_type == "rotate"
|
|
assert widget._interaction_state.rotation == 0.0
|
|
|
|
def test_begin_image_pan(self):
|
|
"""Test beginning an image pan interaction."""
|
|
from pyPhotoAlbum.models import ImageData
|
|
|
|
widget = MockWidget()
|
|
element = Mock(spec=ImageData)
|
|
element.crop_info = (0.0, 0.0, 1.0, 1.0)
|
|
|
|
widget._begin_image_pan(element)
|
|
|
|
assert widget._interaction_state.element == element
|
|
assert widget._interaction_state.interaction_type == "image_pan"
|
|
assert widget._interaction_state.crop_info == (0.0, 0.0, 1.0, 1.0)
|
|
|
|
def test_begin_image_pan_non_image_element(self):
|
|
"""Test that image pan doesn't start for non-ImageData elements."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
|
|
widget._begin_image_pan(element)
|
|
|
|
# Should not set interaction state
|
|
assert widget._interaction_state.element is None
|
|
|
|
def test_end_interaction_creates_move_command(self):
|
|
"""Test that ending a move interaction creates a command."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
|
|
# Simulate movement
|
|
element.position = (10.0, 10.0)
|
|
|
|
widget._end_interaction()
|
|
|
|
# Verify command was executed
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
|
|
def test_end_interaction_creates_resize_command(self):
|
|
"""Test that ending a resize interaction creates a command."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
element.size = (100.0, 100.0)
|
|
|
|
widget._begin_resize(element)
|
|
|
|
# Simulate resize
|
|
element.size = (200.0, 200.0)
|
|
|
|
widget._end_interaction()
|
|
|
|
# Verify command was executed
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
|
|
def test_end_interaction_creates_rotate_command(self):
|
|
"""Test that ending a rotate interaction creates a command."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.rotation = 0.0
|
|
element.position = (0.0, 0.0) # Required by RotateElementCommand
|
|
element.size = (100.0, 100.0) # Required by RotateElementCommand
|
|
|
|
widget._begin_rotate(element)
|
|
|
|
# Simulate rotation
|
|
element.rotation = 45.0
|
|
|
|
widget._end_interaction()
|
|
|
|
# Verify command was executed
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
|
|
def test_end_interaction_no_command_for_insignificant_change(self):
|
|
"""Test that no command is created for insignificant changes."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
|
|
# Insignificant movement
|
|
element.position = (0.05, 0.05)
|
|
|
|
widget._end_interaction()
|
|
|
|
# Verify no command was executed
|
|
widget._mock_window.project.history.execute.assert_not_called()
|
|
|
|
def test_end_interaction_clears_state(self):
|
|
"""Test that ending interaction clears state."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
widget._end_interaction()
|
|
|
|
assert widget._interaction_state.element is None
|
|
assert widget._interaction_state.interaction_type is None
|
|
|
|
def test_end_interaction_without_begin(self):
|
|
"""Test that ending interaction without beginning is safe."""
|
|
widget = MockWidget()
|
|
|
|
widget._end_interaction()
|
|
|
|
# Should not crash or execute commands
|
|
widget._mock_window.project.history.execute.assert_not_called()
|
|
|
|
def test_cancel_interaction(self):
|
|
"""Test canceling an interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
widget._cancel_interaction()
|
|
|
|
assert widget._interaction_state.element is None
|
|
assert widget._interaction_state.interaction_type is None
|
|
|
|
def test_clear_interaction_state(self):
|
|
"""Test clearing interaction state directly."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
widget._clear_interaction_state()
|
|
|
|
assert widget._interaction_state.element is None
|
|
|
|
def test_end_interaction_without_project(self):
|
|
"""Test that ending interaction without project is safe."""
|
|
widget = MockWidget()
|
|
# Remove the project attribute entirely
|
|
delattr(widget._mock_window, "project")
|
|
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
widget._begin_move(element)
|
|
element.position = (10.0, 10.0)
|
|
|
|
widget._end_interaction()
|
|
|
|
# Should clear state without crashing
|
|
assert widget._interaction_state.element is None
|
|
|
|
|
|
class TestMixinIntegrationWithFactory:
|
|
"""Integration tests between mixin and factory."""
|
|
|
|
def test_move_interaction_complete_flow(self):
|
|
"""Test complete flow of a move interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
# Begin
|
|
widget._begin_move(element)
|
|
assert widget._interaction_state.is_valid()
|
|
|
|
# Modify
|
|
element.position = (50.0, 75.0)
|
|
|
|
# End
|
|
widget._end_interaction()
|
|
|
|
# Verify
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
assert not widget._interaction_state.is_valid()
|
|
|
|
def test_resize_interaction_complete_flow(self):
|
|
"""Test complete flow of a resize interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
element.size = (100.0, 100.0)
|
|
|
|
# Begin
|
|
widget._begin_resize(element)
|
|
|
|
# Modify
|
|
element.position = (10.0, 10.0)
|
|
element.size = (200.0, 150.0)
|
|
|
|
# End
|
|
widget._end_interaction()
|
|
|
|
# Verify
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
|
|
def test_rotate_interaction_complete_flow(self):
|
|
"""Test complete flow of a rotate interaction."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.rotation = 0.0
|
|
element.position = (0.0, 0.0) # Required by RotateElementCommand
|
|
element.size = (100.0, 100.0) # Required by RotateElementCommand
|
|
|
|
# Begin
|
|
widget._begin_rotate(element)
|
|
|
|
# Modify
|
|
element.rotation = 90.0
|
|
|
|
# End
|
|
widget._end_interaction()
|
|
|
|
# Verify
|
|
widget._mock_window.project.history.execute.assert_called_once()
|
|
|
|
def test_multiple_interactions_in_sequence(self):
|
|
"""Test multiple interactions in sequence."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
element.size = (100.0, 100.0)
|
|
element.rotation = 0.0
|
|
|
|
# First interaction: move
|
|
widget._begin_move(element)
|
|
element.position = (10.0, 10.0)
|
|
widget._end_interaction()
|
|
|
|
# Second interaction: resize
|
|
widget._begin_resize(element)
|
|
element.size = (200.0, 200.0)
|
|
widget._end_interaction()
|
|
|
|
# Third interaction: rotate
|
|
widget._begin_rotate(element)
|
|
element.rotation = 45.0
|
|
widget._end_interaction()
|
|
|
|
# Should have created 3 commands
|
|
assert widget._mock_window.project.history.execute.call_count == 3
|
|
|
|
def test_interaction_with_cancel(self):
|
|
"""Test interaction flow with cancellation."""
|
|
widget = MockWidget()
|
|
element = Mock(spec=BaseLayoutElement)
|
|
element.position = (0.0, 0.0)
|
|
|
|
# Begin
|
|
widget._begin_move(element)
|
|
element.position = (50.0, 50.0)
|
|
|
|
# Cancel instead of end
|
|
widget._cancel_interaction()
|
|
|
|
# No command should be created
|
|
widget._mock_window.project.history.execute.assert_not_called()
|