Improved undo-redo system
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
Unit tests for z-order operations in pyPhotoAlbum
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData
|
||||
from pyPhotoAlbum.page_layout import PageLayout
|
||||
from pyPhotoAlbum.commands import ChangeZOrderCommand, CommandHistory
|
||||
|
||||
|
||||
class TestZOrderBasics:
|
||||
"""Tests for basic z-order functionality"""
|
||||
|
||||
def test_list_order_is_render_order(self):
|
||||
"""Test that list order determines render order"""
|
||||
layout = PageLayout(width=210, height=297)
|
||||
|
||||
# Add elements in order
|
||||
elem1 = ImageData(x=10, y=10, width=50, height=50)
|
||||
elem2 = TextBoxData(x=20, y=20, width=50, height=50)
|
||||
elem3 = PlaceholderData(x=30, y=30, width=50, height=50)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Verify order
|
||||
assert layout.elements[0] is elem1
|
||||
assert layout.elements[1] is elem2
|
||||
assert layout.elements[2] is elem3
|
||||
|
||||
def test_element_at_end_renders_on_top(self):
|
||||
"""Test that element at end of list renders on top"""
|
||||
layout = PageLayout(width=210, height=297)
|
||||
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = ImageData(x=20, y=20)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
|
||||
# elem2 should be last (on top)
|
||||
assert layout.elements[-1] is elem2
|
||||
assert layout.elements.index(elem2) > layout.elements.index(elem1)
|
||||
|
||||
|
||||
class TestChangeZOrderCommand:
|
||||
"""Tests for ChangeZOrderCommand"""
|
||||
|
||||
def test_move_element_forward(self):
|
||||
"""Test moving an element forward one position"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Move elem1 forward (swap with elem2)
|
||||
cmd = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=1)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements.index(elem1) == 1
|
||||
assert layout.elements.index(elem2) == 0
|
||||
assert layout.elements.index(elem3) == 2
|
||||
|
||||
def test_move_element_backward(self):
|
||||
"""Test moving an element backward one position"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Move elem2 backward (swap with elem1)
|
||||
cmd = ChangeZOrderCommand(layout, elem2, old_index=1, new_index=0)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements.index(elem2) == 0
|
||||
assert layout.elements.index(elem1) == 1
|
||||
assert layout.elements.index(elem3) == 2
|
||||
|
||||
def test_move_to_front(self):
|
||||
"""Test moving an element to the front (end of list)"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Move elem1 to front
|
||||
cmd = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=2)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements[-1] is elem1
|
||||
assert layout.elements.index(elem1) == 2
|
||||
|
||||
def test_move_to_back(self):
|
||||
"""Test moving an element to the back (start of list)"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Move elem3 to back
|
||||
cmd = ChangeZOrderCommand(layout, elem3, old_index=2, new_index=0)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements[0] is elem3
|
||||
assert layout.elements.index(elem3) == 0
|
||||
|
||||
def test_undo_redo(self):
|
||||
"""Test undo/redo functionality"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
original_order = list(layout.elements)
|
||||
|
||||
# Move elem1 forward
|
||||
cmd = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=1)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements.index(elem1) == 1
|
||||
|
||||
# Undo
|
||||
cmd.undo()
|
||||
assert layout.elements == original_order
|
||||
|
||||
# Redo
|
||||
cmd.redo()
|
||||
assert layout.elements.index(elem1) == 1
|
||||
|
||||
def test_command_with_history(self):
|
||||
"""Test ChangeZOrderCommand with CommandHistory"""
|
||||
layout = PageLayout()
|
||||
history = CommandHistory()
|
||||
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Execute command through history
|
||||
cmd = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=2)
|
||||
history.execute(cmd)
|
||||
|
||||
assert layout.elements.index(elem1) == 2
|
||||
assert history.can_undo()
|
||||
|
||||
# Undo through history
|
||||
history.undo()
|
||||
assert layout.elements.index(elem1) == 0
|
||||
assert history.can_redo()
|
||||
|
||||
# Redo through history
|
||||
history.redo()
|
||||
assert layout.elements.index(elem1) == 2
|
||||
|
||||
|
||||
class TestZOrderSerialization:
|
||||
"""Tests for z-order serialization and deserialization"""
|
||||
|
||||
def test_serialize_preserves_order(self):
|
||||
"""Test that serialization preserves element order"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10, z_index=0)
|
||||
elem2 = TextBoxData(x=20, y=20, z_index=1)
|
||||
elem3 = PlaceholderData(x=30, y=30, z_index=2)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Serialize
|
||||
data = layout.serialize()
|
||||
|
||||
# Elements should be in order
|
||||
assert len(data['elements']) == 3
|
||||
assert data['elements'][0]['type'] == 'image'
|
||||
assert data['elements'][1]['type'] == 'textbox'
|
||||
assert data['elements'][2]['type'] == 'placeholder'
|
||||
|
||||
def test_deserialize_sorts_by_zindex(self):
|
||||
"""Test that deserialization sorts by z_index for backward compatibility"""
|
||||
layout = PageLayout()
|
||||
|
||||
# Create data with z_index values out of order
|
||||
data = {
|
||||
'size': (210, 297),
|
||||
'base_width': 210,
|
||||
'is_facing_page': False,
|
||||
'background_color': (1.0, 1.0, 1.0),
|
||||
'elements': [
|
||||
{'type': 'image', 'position': (10, 10), 'size': (50, 50),
|
||||
'rotation': 0, 'z_index': 2, 'image_path': '', 'crop_info': (0, 0, 1, 1)},
|
||||
{'type': 'textbox', 'position': (20, 20), 'size': (50, 50),
|
||||
'rotation': 0, 'z_index': 0, 'text_content': '',
|
||||
'font_settings': {}, 'alignment': 'left'},
|
||||
{'type': 'placeholder', 'position': (30, 30), 'size': (50, 50),
|
||||
'rotation': 0, 'z_index': 1, 'placeholder_type': 'image', 'default_content': ''},
|
||||
]
|
||||
}
|
||||
|
||||
layout.deserialize(data)
|
||||
|
||||
# Elements should be sorted by z_index
|
||||
assert len(layout.elements) == 3
|
||||
assert isinstance(layout.elements[0], TextBoxData) # z_index=0
|
||||
assert isinstance(layout.elements[1], PlaceholderData) # z_index=1
|
||||
assert isinstance(layout.elements[2], ImageData) # z_index=2
|
||||
|
||||
def test_roundtrip_maintains_order(self):
|
||||
"""Test that serialize/deserialize maintains element order"""
|
||||
layout1 = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10, z_index=0)
|
||||
elem2 = TextBoxData(x=20, y=20, z_index=1)
|
||||
elem3 = PlaceholderData(x=30, y=30, z_index=2)
|
||||
|
||||
layout1.add_element(elem1)
|
||||
layout1.add_element(elem2)
|
||||
layout1.add_element(elem3)
|
||||
|
||||
# Serialize and deserialize
|
||||
data = layout1.serialize()
|
||||
layout2 = PageLayout()
|
||||
layout2.deserialize(data)
|
||||
|
||||
# Order should be maintained
|
||||
assert len(layout2.elements) == 3
|
||||
assert isinstance(layout2.elements[0], ImageData)
|
||||
assert isinstance(layout2.elements[1], TextBoxData)
|
||||
assert isinstance(layout2.elements[2], PlaceholderData)
|
||||
|
||||
|
||||
class TestZOrderEdgeCases:
|
||||
"""Tests for z-order edge cases"""
|
||||
|
||||
def test_single_element(self):
|
||||
"""Test operations with single element"""
|
||||
layout = PageLayout()
|
||||
elem = ImageData(x=10, y=10)
|
||||
layout.add_element(elem)
|
||||
|
||||
# Try to move forward (should stay at index 0)
|
||||
cmd = ChangeZOrderCommand(layout, elem, old_index=0, new_index=0)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements.index(elem) == 0
|
||||
|
||||
def test_empty_list(self):
|
||||
"""Test operations with empty list"""
|
||||
layout = PageLayout()
|
||||
assert len(layout.elements) == 0
|
||||
|
||||
def test_move_to_same_position(self):
|
||||
"""Test moving element to its current position"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
|
||||
# Move to same position
|
||||
cmd = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=0)
|
||||
cmd.execute()
|
||||
|
||||
assert layout.elements.index(elem1) == 0
|
||||
assert layout.elements.index(elem2) == 1
|
||||
|
||||
def test_swap_adjacent_elements(self):
|
||||
"""Test swapping two adjacent elements"""
|
||||
layout = PageLayout()
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
|
||||
# Swap by moving elem1 forward
|
||||
elements = layout.elements
|
||||
index1 = elements.index(elem1)
|
||||
index2 = elements.index(elem2)
|
||||
elements[index1], elements[index2] = elements[index2], elements[index1]
|
||||
|
||||
assert layout.elements[0] is elem2
|
||||
assert layout.elements[1] is elem1
|
||||
|
||||
def test_multiple_zorder_changes(self):
|
||||
"""Test multiple z-order changes in sequence"""
|
||||
layout = PageLayout()
|
||||
history = CommandHistory()
|
||||
|
||||
elem1 = ImageData(x=10, y=10)
|
||||
elem2 = TextBoxData(x=20, y=20)
|
||||
elem3 = PlaceholderData(x=30, y=30)
|
||||
|
||||
layout.add_element(elem1)
|
||||
layout.add_element(elem2)
|
||||
layout.add_element(elem3)
|
||||
|
||||
# Move elem1 to front
|
||||
cmd1 = ChangeZOrderCommand(layout, elem1, old_index=0, new_index=2)
|
||||
history.execute(cmd1)
|
||||
assert layout.elements.index(elem1) == 2
|
||||
|
||||
# Move elem2 to front
|
||||
cmd2 = ChangeZOrderCommand(layout, elem2, old_index=0, new_index=2)
|
||||
history.execute(cmd2)
|
||||
assert layout.elements.index(elem2) == 2
|
||||
|
||||
# Undo both
|
||||
history.undo()
|
||||
assert layout.elements.index(elem2) == 0
|
||||
|
||||
history.undo()
|
||||
assert layout.elements.index(elem1) == 0
|
||||
|
||||
|
||||
class TestZOrderCommandSerialization:
|
||||
"""Tests for ChangeZOrderCommand serialization"""
|
||||
|
||||
def test_serialize_command(self):
|
||||
"""Test serializing a ChangeZOrderCommand"""
|
||||
layout = PageLayout()
|
||||
elem = ImageData(x=10, y=10)
|
||||
layout.add_element(elem)
|
||||
|
||||
cmd = ChangeZOrderCommand(layout, elem, old_index=0, new_index=1)
|
||||
|
||||
data = cmd.serialize()
|
||||
|
||||
assert data['type'] == 'change_zorder'
|
||||
assert data['old_index'] == 0
|
||||
assert data['new_index'] == 1
|
||||
assert 'element' in data
|
||||
|
||||
def test_deserialize_command(self):
|
||||
"""Test deserializing a ChangeZOrderCommand"""
|
||||
data = {
|
||||
'type': 'change_zorder',
|
||||
'element': {
|
||||
'type': 'image',
|
||||
'position': (10, 10),
|
||||
'size': (50, 50),
|
||||
'rotation': 0,
|
||||
'z_index': 0,
|
||||
'image_path': '',
|
||||
'crop_info': (0, 0, 1, 1)
|
||||
},
|
||||
'old_index': 0,
|
||||
'new_index': 1
|
||||
}
|
||||
|
||||
cmd = ChangeZOrderCommand.deserialize(data, None)
|
||||
|
||||
assert isinstance(cmd, ChangeZOrderCommand)
|
||||
assert cmd.old_index == 0
|
||||
assert cmd.new_index == 1
|
||||
assert isinstance(cmd.element, ImageData)
|
||||
Reference in New Issue
Block a user