658 lines
21 KiB
Python
658 lines
21 KiB
Python
"""
|
|
Tests for Command pattern implementation
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, MagicMock
|
|
from pyPhotoAlbum.commands import (
|
|
AddElementCommand,
|
|
DeleteElementCommand,
|
|
MoveElementCommand,
|
|
ResizeElementCommand,
|
|
RotateElementCommand,
|
|
AdjustImageCropCommand,
|
|
AlignElementsCommand,
|
|
ResizeElementsCommand,
|
|
ChangeZOrderCommand,
|
|
StateChangeCommand,
|
|
CommandHistory,
|
|
_normalize_asset_path
|
|
)
|
|
from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
from pyPhotoAlbum.project import Project, Page
|
|
|
|
|
|
class TestNormalizeAssetPath:
|
|
"""Test _normalize_asset_path helper function"""
|
|
|
|
def test_normalize_absolute_path(self):
|
|
"""Test converting absolute path to relative"""
|
|
mock_manager = Mock()
|
|
mock_manager.project_folder = "/project"
|
|
|
|
result = _normalize_asset_path("/project/assets/image.jpg", mock_manager)
|
|
assert result == "assets/image.jpg"
|
|
|
|
def test_normalize_relative_path_unchanged(self):
|
|
"""Test relative path stays unchanged"""
|
|
mock_manager = Mock()
|
|
mock_manager.project_folder = "/project"
|
|
|
|
result = _normalize_asset_path("assets/image.jpg", mock_manager)
|
|
assert result == "assets/image.jpg"
|
|
|
|
def test_normalize_no_asset_manager(self):
|
|
"""Test with no asset manager returns unchanged"""
|
|
result = _normalize_asset_path("/path/to/image.jpg", None)
|
|
assert result == "/path/to/image.jpg"
|
|
|
|
def test_normalize_empty_path(self):
|
|
"""Test with empty path"""
|
|
mock_manager = Mock()
|
|
result = _normalize_asset_path("", mock_manager)
|
|
assert result == ""
|
|
|
|
|
|
class TestAddElementCommand:
|
|
"""Test AddElementCommand"""
|
|
|
|
def test_add_element_execute(self):
|
|
"""Test adding element to layout"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
assert len(layout.elements) == 0
|
|
|
|
cmd.execute()
|
|
|
|
assert len(layout.elements) == 1
|
|
assert element in layout.elements
|
|
|
|
def test_add_element_undo(self):
|
|
"""Test undoing element addition"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
cmd.execute()
|
|
assert len(layout.elements) == 1
|
|
|
|
cmd.undo()
|
|
|
|
assert len(layout.elements) == 0
|
|
|
|
def test_add_element_redo(self):
|
|
"""Test redoing element addition"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
cmd.execute()
|
|
cmd.undo()
|
|
|
|
cmd.redo()
|
|
|
|
assert len(layout.elements) == 1
|
|
assert element in layout.elements
|
|
|
|
def test_add_element_serialization(self):
|
|
"""Test serializing add element command"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
cmd.execute()
|
|
|
|
data = cmd.serialize()
|
|
|
|
assert data['type'] == 'add_element'
|
|
assert 'element' in data
|
|
assert data['executed'] is True
|
|
|
|
def test_add_element_with_asset_manager(self):
|
|
"""Test add element with asset manager reference"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
mock_asset_manager = Mock()
|
|
mock_asset_manager.project_folder = "/project"
|
|
mock_asset_manager.acquire_reference = Mock()
|
|
|
|
cmd = AddElementCommand(layout, element, asset_manager=mock_asset_manager)
|
|
|
|
# Should acquire reference on creation
|
|
assert mock_asset_manager.acquire_reference.called
|
|
|
|
|
|
class TestDeleteElementCommand:
|
|
"""Test DeleteElementCommand"""
|
|
|
|
def test_delete_element_execute(self):
|
|
"""Test deleting element from layout"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
layout.add_element(element)
|
|
|
|
cmd = DeleteElementCommand(layout, element)
|
|
cmd.execute()
|
|
|
|
assert len(layout.elements) == 0
|
|
assert element not in layout.elements
|
|
|
|
def test_delete_element_undo(self):
|
|
"""Test undoing element deletion"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
layout.add_element(element)
|
|
|
|
cmd = DeleteElementCommand(layout, element)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert len(layout.elements) == 1
|
|
assert element in layout.elements
|
|
|
|
def test_delete_element_serialization(self):
|
|
"""Test serializing delete element command"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
layout.add_element(element)
|
|
|
|
cmd = DeleteElementCommand(layout, element)
|
|
data = cmd.serialize()
|
|
|
|
assert data['type'] == 'delete_element'
|
|
assert 'element' in data
|
|
|
|
|
|
class TestMoveElementCommand:
|
|
"""Test MoveElementCommand"""
|
|
|
|
def test_move_element_execute(self):
|
|
"""Test moving element"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200))
|
|
cmd.execute()
|
|
|
|
assert element.position == (200, 200)
|
|
|
|
def test_move_element_undo(self):
|
|
"""Test undoing element move"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200))
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert element.position == (100, 100)
|
|
|
|
def test_move_element_serialization(self):
|
|
"""Test serializing move command"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200))
|
|
data = cmd.serialize()
|
|
|
|
assert data['type'] == 'move_element'
|
|
assert data['old_position'] == (100, 100)
|
|
assert data['new_position'] == (200, 200)
|
|
|
|
|
|
class TestResizeElementCommand:
|
|
"""Test ResizeElementCommand"""
|
|
|
|
def test_resize_element_execute(self):
|
|
"""Test resizing element"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = ResizeElementCommand(
|
|
element,
|
|
old_position=(100, 100),
|
|
old_size=(200, 150),
|
|
new_position=(100, 100),
|
|
new_size=(300, 225)
|
|
)
|
|
cmd.execute()
|
|
|
|
assert element.size == (300, 225)
|
|
|
|
def test_resize_element_undo(self):
|
|
"""Test undoing element resize"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = ResizeElementCommand(
|
|
element,
|
|
old_position=(100, 100),
|
|
old_size=(200, 150),
|
|
new_position=(100, 100),
|
|
new_size=(300, 225)
|
|
)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert element.size == (200, 150)
|
|
|
|
def test_resize_changes_position(self):
|
|
"""Test resize that also changes position"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = ResizeElementCommand(
|
|
element,
|
|
old_position=(100, 100),
|
|
old_size=(200, 150),
|
|
new_position=(90, 90),
|
|
new_size=(220, 165)
|
|
)
|
|
cmd.execute()
|
|
|
|
assert element.position == (90, 90)
|
|
assert element.size == (220, 165)
|
|
|
|
|
|
class TestRotateElementCommand:
|
|
"""Test RotateElementCommand"""
|
|
|
|
def test_rotate_element_execute(self):
|
|
"""Test rotating element"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
element.rotation = 0
|
|
|
|
cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90)
|
|
cmd.execute()
|
|
|
|
assert element.rotation == 90
|
|
|
|
def test_rotate_element_undo(self):
|
|
"""Test undoing element rotation"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
element.rotation = 0
|
|
|
|
cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert element.rotation == 0
|
|
|
|
def test_rotate_element_serialization(self):
|
|
"""Test serializing rotate command"""
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = RotateElementCommand(element, old_rotation=0, new_rotation=45)
|
|
data = cmd.serialize()
|
|
|
|
assert data['type'] == 'rotate_element'
|
|
assert data['old_rotation'] == 0
|
|
assert data['new_rotation'] == 45
|
|
|
|
|
|
class TestAdjustImageCropCommand:
|
|
"""Test AdjustImageCropCommand"""
|
|
|
|
def test_adjust_crop_execute(self):
|
|
"""Test adjusting image crop"""
|
|
element = ImageData(
|
|
image_path="/test.jpg",
|
|
x=100, y=100,
|
|
width=200, height=150,
|
|
crop_info={'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0}
|
|
)
|
|
|
|
new_crop = {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}
|
|
cmd = AdjustImageCropCommand(
|
|
element,
|
|
old_crop_info=element.crop_info.copy(),
|
|
new_crop_info=new_crop
|
|
)
|
|
cmd.execute()
|
|
|
|
assert element.crop_info == new_crop
|
|
|
|
def test_adjust_crop_undo(self):
|
|
"""Test undoing crop adjustment"""
|
|
old_crop = {'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0}
|
|
element = ImageData(
|
|
image_path="/test.jpg",
|
|
x=100, y=100,
|
|
width=200, height=150,
|
|
crop_info=old_crop.copy()
|
|
)
|
|
|
|
new_crop = {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}
|
|
cmd = AdjustImageCropCommand(element, old_crop_info=old_crop, new_crop_info=new_crop)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert element.crop_info == old_crop
|
|
|
|
|
|
class TestAlignElementsCommand:
|
|
"""Test AlignElementsCommand"""
|
|
|
|
def test_align_elements_execute(self):
|
|
"""Test aligning elements"""
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=150, width=100, height=100)
|
|
|
|
# Set new positions before creating command
|
|
element2.position = (100, 150) # Align left
|
|
|
|
# Command expects list of (element, old_position) tuples
|
|
changes = [(element1, (100, 100)), (element2, (200, 150))]
|
|
|
|
cmd = AlignElementsCommand(changes)
|
|
cmd.execute()
|
|
|
|
# Execute does nothing (positions already set), check they remain
|
|
assert element1.position == (100, 100)
|
|
assert element2.position == (100, 150)
|
|
|
|
def test_align_elements_undo(self):
|
|
"""Test undoing alignment"""
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=150, width=100, height=100)
|
|
|
|
# Set new positions before creating command
|
|
element2.position = (100, 150) # Align left
|
|
|
|
# Command expects list of (element, old_position) tuples
|
|
changes = [(element1, (100, 100)), (element2, (200, 150))]
|
|
|
|
cmd = AlignElementsCommand(changes)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
# Should restore old positions
|
|
assert element1.position == (100, 100)
|
|
assert element2.position == (200, 150)
|
|
|
|
|
|
class TestResizeElementsCommand:
|
|
"""Test ResizeElementsCommand"""
|
|
|
|
def test_resize_elements_execute(self):
|
|
"""Test resizing multiple elements"""
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=150, height=150)
|
|
|
|
# Set new sizes before creating command
|
|
element1.size = (200, 200)
|
|
element2.size = (300, 300)
|
|
|
|
# Command expects list of (element, old_position, old_size) tuples
|
|
changes = [
|
|
(element1, (100, 100), (100, 100)),
|
|
(element2, (200, 200), (150, 150))
|
|
]
|
|
|
|
cmd = ResizeElementsCommand(changes)
|
|
cmd.execute()
|
|
|
|
# Execute does nothing (sizes already set), check they remain
|
|
assert element1.size == (200, 200)
|
|
assert element2.size == (300, 300)
|
|
|
|
def test_resize_elements_undo(self):
|
|
"""Test undoing multiple element resize"""
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=150, height=150)
|
|
|
|
# Set new sizes before creating command
|
|
element1.size = (200, 200)
|
|
element2.size = (300, 300)
|
|
|
|
# Command expects list of (element, old_position, old_size) tuples
|
|
changes = [
|
|
(element1, (100, 100), (100, 100)),
|
|
(element2, (200, 200), (150, 150))
|
|
]
|
|
|
|
cmd = ResizeElementsCommand(changes)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
# Should restore old sizes
|
|
assert element1.size == (100, 100)
|
|
assert element2.size == (150, 150)
|
|
|
|
|
|
class TestChangeZOrderCommand:
|
|
"""Test ChangeZOrderCommand"""
|
|
|
|
def test_change_zorder_execute(self):
|
|
"""Test changing z-order"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=120, y=120, width=100, height=100)
|
|
|
|
layout.add_element(element1)
|
|
layout.add_element(element2)
|
|
|
|
# Move element1 to front (swap order)
|
|
cmd = ChangeZOrderCommand(layout, element1, 0, 1)
|
|
cmd.execute()
|
|
|
|
assert layout.elements[1] == element1
|
|
|
|
def test_change_zorder_undo(self):
|
|
"""Test undoing z-order change"""
|
|
layout = PageLayout(width=210, height=297)
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=120, y=120, width=100, height=100)
|
|
|
|
layout.add_element(element1)
|
|
layout.add_element(element2)
|
|
|
|
cmd = ChangeZOrderCommand(layout, element1, 0, 1)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert layout.elements[0] == element1
|
|
|
|
|
|
class TestStateChangeCommand:
|
|
"""Test StateChangeCommand for generic state changes"""
|
|
|
|
def test_state_change_undo(self):
|
|
"""Test undoing state change"""
|
|
element = TextBoxData(
|
|
text_content="Old Text",
|
|
x=100, y=100,
|
|
width=200, height=100
|
|
)
|
|
|
|
# Define restore function
|
|
def restore_state(state):
|
|
element.text_content = state['text_content']
|
|
|
|
old_state = {'text_content': 'Old Text'}
|
|
new_state = {'text_content': 'New Text'}
|
|
|
|
# Apply new state first
|
|
element.text_content = 'New Text'
|
|
|
|
cmd = StateChangeCommand(
|
|
description="Change text",
|
|
restore_func=restore_state,
|
|
before_state=old_state,
|
|
after_state=new_state
|
|
)
|
|
|
|
# Undo should restore old state
|
|
cmd.undo()
|
|
assert element.text_content == 'Old Text'
|
|
|
|
def test_state_change_redo(self):
|
|
"""Test redoing state change"""
|
|
element = TextBoxData(
|
|
text_content="Old Text",
|
|
x=100, y=100,
|
|
width=200, height=100
|
|
)
|
|
|
|
# Define restore function
|
|
def restore_state(state):
|
|
element.text_content = state['text_content']
|
|
|
|
old_state = {'text_content': 'Old Text'}
|
|
new_state = {'text_content': 'New Text'}
|
|
|
|
# Apply new state first
|
|
element.text_content = 'New Text'
|
|
|
|
cmd = StateChangeCommand(
|
|
description="Change text",
|
|
restore_func=restore_state,
|
|
before_state=old_state,
|
|
after_state=new_state
|
|
)
|
|
|
|
# Undo then redo
|
|
cmd.undo()
|
|
assert element.text_content == 'Old Text'
|
|
|
|
cmd.redo()
|
|
assert element.text_content == 'New Text'
|
|
|
|
def test_state_change_serialization(self):
|
|
"""Test serializing state change command"""
|
|
def restore_func(state):
|
|
pass
|
|
|
|
cmd = StateChangeCommand(
|
|
description="Test operation",
|
|
restore_func=restore_func,
|
|
before_state={'test': 'before'},
|
|
after_state={'test': 'after'}
|
|
)
|
|
|
|
data = cmd.serialize()
|
|
|
|
assert data['type'] == 'state_change'
|
|
assert data['description'] == 'Test operation'
|
|
|
|
|
|
class TestCommandHistory:
|
|
"""Test CommandHistory for undo/redo management"""
|
|
|
|
def test_history_execute_command(self):
|
|
"""Test executing command through history"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
history.execute(cmd)
|
|
|
|
assert len(layout.elements) == 1
|
|
assert history.can_undo()
|
|
assert not history.can_redo()
|
|
|
|
def test_history_undo(self):
|
|
"""Test undo through history"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
history.execute(cmd)
|
|
|
|
history.undo()
|
|
|
|
assert len(layout.elements) == 0
|
|
assert not history.can_undo()
|
|
assert history.can_redo()
|
|
|
|
def test_history_redo(self):
|
|
"""Test redo through history"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd = AddElementCommand(layout, element)
|
|
history.execute(cmd)
|
|
history.undo()
|
|
|
|
history.redo()
|
|
|
|
assert len(layout.elements) == 1
|
|
assert history.can_undo()
|
|
assert not history.can_redo()
|
|
|
|
def test_history_multiple_commands(self):
|
|
"""Test history with multiple commands"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=100, height=100)
|
|
|
|
history.execute(AddElementCommand(layout, element1))
|
|
history.execute(AddElementCommand(layout, element2))
|
|
|
|
assert len(layout.elements) == 2
|
|
|
|
history.undo()
|
|
assert len(layout.elements) == 1
|
|
|
|
history.undo()
|
|
assert len(layout.elements) == 0
|
|
|
|
def test_history_clears_redo_on_new_command(self):
|
|
"""Test that new command clears redo stack"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
|
|
element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
|
|
element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=100, height=100)
|
|
|
|
history.execute(AddElementCommand(layout, element1))
|
|
history.undo()
|
|
|
|
assert history.can_redo()
|
|
|
|
# Execute new command should clear redo stack
|
|
history.execute(AddElementCommand(layout, element2))
|
|
|
|
assert not history.can_redo()
|
|
|
|
def test_history_clear(self):
|
|
"""Test clearing history"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
history.execute(AddElementCommand(layout, element))
|
|
|
|
history.clear()
|
|
|
|
assert not history.can_undo()
|
|
assert not history.can_redo()
|
|
|
|
def test_history_max_size(self):
|
|
"""Test history respects max size limit"""
|
|
history = CommandHistory(max_history=3)
|
|
layout = PageLayout(width=210, height=297)
|
|
|
|
for i in range(5):
|
|
element = ImageData(image_path=f"/test{i}.jpg", x=i*10, y=i*10, width=100, height=100)
|
|
history.execute(AddElementCommand(layout, element))
|
|
|
|
# Should only have 3 commands in history (max_history)
|
|
undo_count = 0
|
|
while history.can_undo():
|
|
history.undo()
|
|
undo_count += 1
|
|
|
|
assert undo_count == 3
|