pyPhotoAlbum/tests/test_zorder.py
2025-11-11 16:02:02 +00:00

382 lines
13 KiB
Python

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