All checks were successful
Python CI / test (push) Successful in 1m40s
Lint / lint (push) Successful in 1m29s
Tests / test (3.11) (push) Successful in 1m48s
Tests / test (3.12) (push) Successful in 1m51s
Tests / test (3.13) (push) Successful in 1m46s
Tests / test (3.14) (push) Successful in 1m29s
807 lines
27 KiB
Python
Executable File
807 lines
27 KiB
Python
Executable File
"""
|
|
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
|
|
element.pil_rotation_90 = 0
|
|
|
|
cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90)
|
|
cmd.execute()
|
|
|
|
# After rotation refactoring, ImageData keeps rotation at 0 and uses pil_rotation_90
|
|
assert element.rotation == 0
|
|
assert element.pil_rotation_90 == 1 # 90 degrees = 1 rotation
|
|
# Position and size should be swapped for 90 degree rotation
|
|
assert element.size == (150, 200) # width and height swapped
|
|
|
|
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
|
|
element.pil_rotation_90 = 0
|
|
original_size = element.size
|
|
original_position = element.position
|
|
|
|
cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90)
|
|
cmd.execute()
|
|
|
|
cmd.undo()
|
|
|
|
assert element.rotation == 0
|
|
assert element.pil_rotation_90 == 0
|
|
assert element.size == original_size
|
|
assert element.position == original_position
|
|
|
|
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
|
|
|
|
def test_history_serialize_deserialize_add_element(self):
|
|
"""Test serializing and deserializing history with AddElementCommand"""
|
|
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)
|
|
|
|
# Serialize
|
|
data = history.serialize()
|
|
assert len(data["undo_stack"]) == 1
|
|
assert data["undo_stack"][0]["type"] == "add_element"
|
|
|
|
# Create mock project for deserialization
|
|
mock_project = Mock()
|
|
mock_project.pages = [Mock(layout=layout)]
|
|
|
|
# Deserialize
|
|
new_history = CommandHistory()
|
|
new_history.deserialize(data, mock_project)
|
|
|
|
assert len(new_history.undo_stack) == 1
|
|
assert len(new_history.redo_stack) == 0
|
|
|
|
def test_history_serialize_deserialize_all_command_types(self):
|
|
"""Test serializing and deserializing all command types through history"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
|
|
# Create elements and add them to layout first
|
|
img = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
txt = TextBoxData(text_content="Test", x=50, y=50, width=100, height=50)
|
|
img2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=100, height=100)
|
|
|
|
# Build commands - serialize each type without executing them
|
|
# (we only care about serialization/deserialization, not execution)
|
|
cmd1 = AddElementCommand(layout, img)
|
|
cmd1.serialize() # Ensure it can serialize
|
|
|
|
cmd2 = DeleteElementCommand(layout, txt)
|
|
cmd2.serialize()
|
|
|
|
cmd3 = MoveElementCommand(img, (100, 100), (150, 150))
|
|
cmd3.serialize()
|
|
|
|
cmd4 = ResizeElementCommand(img, (100, 100), (200, 150), (120, 120), (180, 130))
|
|
cmd4.serialize()
|
|
|
|
cmd5 = RotateElementCommand(img, 0, 90)
|
|
cmd5.serialize()
|
|
|
|
cmd6 = AdjustImageCropCommand(img, (0, 0, 1, 1), (0.1, 0.1, 0.9, 0.9))
|
|
cmd6.serialize()
|
|
|
|
cmd7 = AlignElementsCommand([(img, (100, 100))])
|
|
cmd7.serialize()
|
|
|
|
cmd8 = ResizeElementsCommand([(img, (100, 100), (200, 150))])
|
|
cmd8.serialize()
|
|
|
|
layout.add_element(img2)
|
|
cmd9 = ChangeZOrderCommand(layout, img2, 0, 0)
|
|
cmd9.serialize()
|
|
|
|
# Manually build serialized history data
|
|
data = {
|
|
"undo_stack": [
|
|
cmd1.serialize(),
|
|
cmd2.serialize(),
|
|
cmd3.serialize(),
|
|
cmd4.serialize(),
|
|
cmd5.serialize(),
|
|
cmd6.serialize(),
|
|
cmd7.serialize(),
|
|
cmd8.serialize(),
|
|
cmd9.serialize(),
|
|
],
|
|
"redo_stack": [],
|
|
"max_history": 100,
|
|
}
|
|
|
|
# Create mock project
|
|
mock_project = Mock()
|
|
mock_project.pages = [Mock(layout=layout)]
|
|
|
|
# Deserialize
|
|
new_history = CommandHistory()
|
|
new_history.deserialize(data, mock_project)
|
|
|
|
assert len(new_history.undo_stack) == 9
|
|
assert new_history.undo_stack[0].__class__.__name__ == "AddElementCommand"
|
|
assert new_history.undo_stack[1].__class__.__name__ == "DeleteElementCommand"
|
|
assert new_history.undo_stack[2].__class__.__name__ == "MoveElementCommand"
|
|
assert new_history.undo_stack[3].__class__.__name__ == "ResizeElementCommand"
|
|
assert new_history.undo_stack[4].__class__.__name__ == "RotateElementCommand"
|
|
assert new_history.undo_stack[5].__class__.__name__ == "AdjustImageCropCommand"
|
|
assert new_history.undo_stack[6].__class__.__name__ == "AlignElementsCommand"
|
|
assert new_history.undo_stack[7].__class__.__name__ == "ResizeElementsCommand"
|
|
assert new_history.undo_stack[8].__class__.__name__ == "ChangeZOrderCommand"
|
|
|
|
def test_history_deserialize_unknown_command_type(self):
|
|
"""Test deserializing unknown command type returns None and continues"""
|
|
history = CommandHistory()
|
|
mock_project = Mock()
|
|
mock_project.pages = []
|
|
|
|
data = {
|
|
"undo_stack": [
|
|
{"type": "unknown_command", "data": "test"},
|
|
{"type": "add_element", "element": ImageData().serialize(), "executed": True},
|
|
],
|
|
"redo_stack": [],
|
|
"max_history": 100,
|
|
}
|
|
|
|
# Should not raise exception, just skip unknown command
|
|
history.deserialize(data, mock_project)
|
|
|
|
# Should only have the valid command
|
|
assert len(history.undo_stack) == 1
|
|
assert history.undo_stack[0].__class__.__name__ == "AddElementCommand"
|
|
|
|
def test_history_deserialize_malformed_command(self):
|
|
"""Test deserializing malformed command handles exception gracefully"""
|
|
history = CommandHistory()
|
|
mock_project = Mock()
|
|
|
|
data = {
|
|
"undo_stack": [
|
|
{"type": "add_element"}, # Missing required 'element' field
|
|
{
|
|
"type": "move_element",
|
|
"element": ImageData().serialize(),
|
|
"old_position": (0, 0),
|
|
"new_position": (10, 10),
|
|
},
|
|
],
|
|
"redo_stack": [],
|
|
"max_history": 100,
|
|
}
|
|
|
|
# Should not raise exception, just skip malformed command
|
|
history.deserialize(data, mock_project)
|
|
|
|
# Should only have the valid command
|
|
assert len(history.undo_stack) == 1
|
|
assert history.undo_stack[0].__class__.__name__ == "MoveElementCommand"
|
|
|
|
def test_history_serialize_deserialize_with_redo_stack(self):
|
|
"""Test serializing and deserializing with items in redo stack"""
|
|
history = CommandHistory()
|
|
layout = PageLayout(width=210, height=297)
|
|
element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
|
|
|
|
cmd1 = AddElementCommand(layout, element)
|
|
cmd2 = MoveElementCommand(element, (100, 100), (150, 150))
|
|
|
|
history.execute(cmd1)
|
|
history.execute(cmd2)
|
|
history.undo() # Move cmd2 to redo stack
|
|
|
|
# Serialize
|
|
data = history.serialize()
|
|
assert len(data["undo_stack"]) == 1
|
|
assert len(data["redo_stack"]) == 1
|
|
|
|
# Deserialize
|
|
mock_project = Mock()
|
|
mock_project.pages = []
|
|
new_history = CommandHistory()
|
|
new_history.deserialize(data, mock_project)
|
|
|
|
assert len(new_history.undo_stack) == 1
|
|
assert len(new_history.redo_stack) == 1
|