pyPhotoAlbum/tests/test_interaction_undo_refactored.py
Duncan Tourolle f6ed11b0bc
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
black formatting
2025-11-27 23:07:16 +01:00

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()