""" Unit tests for pyPhotoAlbum alignment system """ import pytest from pyPhotoAlbum.alignment import AlignmentManager from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData class TestAlignmentManager: """Tests for AlignmentManager class""" def test_get_bounds_empty_list(self): """Test get_bounds with empty list""" elements = [] bounds = AlignmentManager.get_bounds(elements) assert bounds == (0, 0, 0, 0) def test_get_bounds_single_element(self): """Test get_bounds with single element""" elem = ImageData(x=10, y=20, width=100, height=50) bounds = AlignmentManager.get_bounds([elem]) # min_x, min_y, max_x, max_y assert bounds == (10, 20, 110, 70) def test_get_bounds_multiple_elements(self): """Test get_bounds with multiple elements""" elem1 = ImageData(x=10, y=20, width=100, height=50) elem2 = ImageData(x=50, y=10, width=80, height=60) elem3 = ImageData(x=5, y=30, width=90, height=40) bounds = AlignmentManager.get_bounds([elem1, elem2, elem3]) # min_x = 5, min_y = 10, max_x = 130 (50+80), max_y = 70 (10+60 or 20+50) assert bounds[0] == 5 # min_x assert bounds[1] == 10 # min_y assert bounds[2] == 130 # max_x assert bounds[3] == 70 # max_y def test_align_left_empty_list(self): """Test align_left with empty list""" changes = AlignmentManager.align_left([]) assert changes == [] def test_align_left_single_element(self): """Test align_left with single element""" elem = ImageData(x=50, y=50, width=100, height=100) changes = AlignmentManager.align_left([elem]) assert changes == [] assert elem.position == (50, 50) # Should not change def test_align_left_multiple_elements(self): """Test align_left with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) changes = AlignmentManager.align_left([elem1, elem2, elem3]) # All should align to x=30 (leftmost) assert elem1.position == (30, 20) assert elem2.position == (30, 40) assert elem3.position == (30, 60) # Check undo information assert len(changes) == 3 assert changes[0] == (elem1, (50, 20)) assert changes[1] == (elem2, (30, 40)) assert changes[2] == (elem3, (70, 60)) def test_align_right_multiple_elements(self): """Test align_right with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) # right edge at 150 elem2 = ImageData(x=30, y=40, width=80, height=60) # right edge at 110 elem3 = ImageData(x=70, y=60, width=90, height=40) # right edge at 160 changes = AlignmentManager.align_right([elem1, elem2, elem3]) # All right edges should align to x=160 (rightmost) assert elem1.position[0] == 60 # 160 - 100 assert elem2.position[0] == 80 # 160 - 80 assert elem3.position[0] == 70 # 160 - 90 # Y positions should not change assert elem1.position[1] == 20 assert elem2.position[1] == 40 assert elem3.position[1] == 60 def test_align_top_multiple_elements(self): """Test align_top with multiple elements""" elem1 = ImageData(x=50, y=30, width=100, height=50) elem2 = ImageData(x=30, y=20, width=80, height=60) elem3 = ImageData(x=70, y=40, width=90, height=40) changes = AlignmentManager.align_top([elem1, elem2, elem3]) # All should align to y=20 (topmost) assert elem1.position[1] == 20 assert elem2.position[1] == 20 assert elem3.position[1] == 20 # X positions should not change assert elem1.position[0] == 50 assert elem2.position[0] == 30 assert elem3.position[0] == 70 def test_align_bottom_multiple_elements(self): """Test align_bottom with multiple elements""" elem1 = ImageData(x=50, y=30, width=100, height=50) # bottom at 80 elem2 = ImageData(x=30, y=20, width=80, height=60) # bottom at 80 elem3 = ImageData(x=70, y=40, width=90, height=50) # bottom at 90 changes = AlignmentManager.align_bottom([elem1, elem2, elem3]) # All bottom edges should align to y=90 (bottommost) assert elem1.position[1] == 40 # 90 - 50 assert elem2.position[1] == 30 # 90 - 60 assert elem3.position[1] == 40 # 90 - 50 # X positions should not change assert elem1.position[0] == 50 assert elem2.position[0] == 30 assert elem3.position[0] == 70 def test_align_horizontal_center_multiple_elements(self): """Test align_horizontal_center with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) # center at 100 elem2 = ImageData(x=30, y=40, width=80, height=60) # center at 70 elem3 = ImageData(x=70, y=60, width=60, height=40) # center at 100 changes = AlignmentManager.align_horizontal_center([elem1, elem2, elem3]) # Average center = (100 + 70 + 100) / 3 = 90 # All elements should center at x=90 assert abs(elem1.position[0] + elem1.size[0]/2 - 90) < 0.01 assert abs(elem2.position[0] + elem2.size[0]/2 - 90) < 0.01 assert abs(elem3.position[0] + elem3.size[0]/2 - 90) < 0.01 # Y positions should not change assert elem1.position[1] == 20 assert elem2.position[1] == 40 assert elem3.position[1] == 60 def test_align_vertical_center_multiple_elements(self): """Test align_vertical_center with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) # center at 45 elem2 = ImageData(x=30, y=40, width=80, height=60) # center at 70 elem3 = ImageData(x=70, y=30, width=60, height=40) # center at 50 changes = AlignmentManager.align_vertical_center([elem1, elem2, elem3]) # Average center = (45 + 70 + 50) / 3 = 55 # All elements should center at y=55 assert abs(elem1.position[1] + elem1.size[1]/2 - 55) < 0.01 assert abs(elem2.position[1] + elem2.size[1]/2 - 55) < 0.01 assert abs(elem3.position[1] + elem3.size[1]/2 - 55) < 0.01 # X positions should not change assert elem1.position[0] == 50 assert elem2.position[0] == 30 assert elem3.position[0] == 70 def test_make_same_size_empty_list(self): """Test make_same_size with empty list""" changes = AlignmentManager.make_same_size([]) assert changes == [] def test_make_same_size_single_element(self): """Test make_same_size with single element""" elem = ImageData(x=50, y=50, width=100, height=100) changes = AlignmentManager.make_same_size([elem]) assert changes == [] assert elem.size == (100, 100) # Should not change def test_make_same_size_multiple_elements(self): """Test make_same_size with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) changes = AlignmentManager.make_same_size([elem1, elem2, elem3]) # All should match elem1's size assert elem1.size == (100, 50) assert elem2.size == (100, 50) assert elem3.size == (100, 50) # Check undo information (only elem2 and elem3 change) assert len(changes) == 2 assert changes[0][0] == elem2 assert changes[0][2] == (80, 60) # old size assert changes[1][0] == elem3 assert changes[1][2] == (90, 40) # old size def test_make_same_width_multiple_elements(self): """Test make_same_width with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) changes = AlignmentManager.make_same_width([elem1, elem2, elem3]) # All widths should match elem1 assert elem1.size[0] == 100 assert elem2.size[0] == 100 assert elem3.size[0] == 100 # Heights should not change assert elem1.size[1] == 50 assert elem2.size[1] == 60 assert elem3.size[1] == 40 def test_make_same_height_multiple_elements(self): """Test make_same_height with multiple elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) changes = AlignmentManager.make_same_height([elem1, elem2, elem3]) # All heights should match elem1 assert elem1.size[1] == 50 assert elem2.size[1] == 50 assert elem3.size[1] == 50 # Widths should not change assert elem1.size[0] == 100 assert elem2.size[0] == 80 assert elem3.size[0] == 90 def test_distribute_horizontally_too_few_elements(self): """Test distribute_horizontally with less than 3 elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) changes = AlignmentManager.distribute_horizontally([elem1, elem2]) assert changes == [] def test_distribute_horizontally_multiple_elements(self): """Test distribute_horizontally with multiple elements""" elem1 = ImageData(x=0, y=20, width=100, height=50) elem2 = ImageData(x=50, y=40, width=80, height=60) elem3 = ImageData(x=200, y=60, width=90, height=40) changes = AlignmentManager.distribute_horizontally([elem1, elem2, elem3]) # Elements should be distributed evenly by their left edges # min_x = 0, max_x = 200, span = 200 # spacing = 200 / (3-1) = 100 positions = [elem.position[0] for elem in [elem1, elem2, elem3]] sorted_positions = sorted(positions) assert sorted_positions[0] == 0 assert sorted_positions[1] == 100 assert sorted_positions[2] == 200 def test_distribute_vertically_multiple_elements(self): """Test distribute_vertically with multiple elements""" elem1 = ImageData(x=20, y=0, width=100, height=50) elem2 = ImageData(x=40, y=50, width=80, height=60) elem3 = ImageData(x=60, y=300, width=90, height=40) changes = AlignmentManager.distribute_vertically([elem1, elem2, elem3]) # Elements should be distributed evenly by their top edges # min_y = 0, max_y = 300, span = 300 # spacing = 300 / (3-1) = 150 positions = [elem.position[1] for elem in [elem1, elem2, elem3]] sorted_positions = sorted(positions) assert sorted_positions[0] == 0 assert sorted_positions[1] == 150 assert sorted_positions[2] == 300 def test_space_horizontally_too_few_elements(self): """Test space_horizontally with less than 3 elements""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=200, y=40, width=80, height=60) changes = AlignmentManager.space_horizontally([elem1, elem2]) assert changes == [] def test_space_horizontally_multiple_elements(self): """Test space_horizontally with multiple elements""" elem1 = ImageData(x=0, y=20, width=100, height=50) elem2 = ImageData(x=150, y=40, width=50, height=60) elem3 = ImageData(x=250, y=60, width=100, height=40) changes = AlignmentManager.space_horizontally([elem1, elem2, elem3]) # Total width = 100 + 50 + 100 = 250 # Span = 0 to 350 (250 + 100 from elem3) # Available space = 350 - 0 - 250 = 100 # Spacing = 100 / (3-1) = 50 # After sorting by x: elem1 at 0, elem2 after 100+50=150, elem3 after 150+50+50=250 sorted_elements = sorted([elem1, elem2, elem3], key=lambda e: e.position[0]) assert sorted_elements[0].position[0] == 0 assert sorted_elements[1].position[0] == 150 # 0 + 100 + 50 assert sorted_elements[2].position[0] == 250 # 150 + 50 + 50 def test_space_vertically_multiple_elements(self): """Test space_vertically with multiple elements""" elem1 = ImageData(x=20, y=0, width=100, height=50) elem2 = ImageData(x=40, y=100, width=80, height=30) elem3 = ImageData(x=60, y=200, width=90, height=50) changes = AlignmentManager.space_vertically([elem1, elem2, elem3]) # Total height = 50 + 30 + 50 = 130 # Span = 0 to 250 (200 + 50 from elem3) # Available space = 250 - 0 - 130 = 120 # Spacing = 120 / (3-1) = 60 # After sorting by y: elem1 at 0, elem2 after 50+60=110, elem3 after 110+30+60=200 sorted_elements = sorted([elem1, elem2, elem3], key=lambda e: e.position[1]) assert sorted_elements[0].position[1] == 0 assert sorted_elements[1].position[1] == 110 # 0 + 50 + 60 assert sorted_elements[2].position[1] == 200 # 110 + 30 + 60 def test_alignment_with_different_element_types(self): """Test alignment works with different element types""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = PlaceholderData(placeholder_type="image", x=30, y=40, width=80, height=60) elem3 = TextBoxData(text_content="Test", x=70, y=60, width=90, height=40) # Test align_left changes = AlignmentManager.align_left([elem1, elem2, elem3]) assert elem1.position[0] == 30 assert elem2.position[0] == 30 assert elem3.position[0] == 30 def test_undo_information_completeness(self): """Test that undo information contains all necessary data""" elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) # Test position changes changes = AlignmentManager.align_left([elem1, elem2, elem3]) for change in changes: assert len(change) == 2 # (element, old_position) assert isinstance(change[0], ImageData) assert isinstance(change[1], tuple) assert len(change[1]) == 2 # (x, y) # Test size changes elem1 = ImageData(x=50, y=20, width=100, height=50) elem2 = ImageData(x=30, y=40, width=80, height=60) elem3 = ImageData(x=70, y=60, width=90, height=40) changes = AlignmentManager.make_same_size([elem1, elem2, elem3]) for change in changes: assert len(change) == 3 # (element, old_position, old_size) assert isinstance(change[0], ImageData) assert isinstance(change[1], tuple) assert len(change[1]) == 2 # (x, y) assert isinstance(change[2], tuple) assert len(change[2]) == 2 # (width, height) def test_alignment_preserves_unaffected_properties(self): """Test that alignment operations only change intended properties""" elem1 = ImageData(x=50, y=20, width=100, height=50, rotation=45, z_index=5) elem2 = ImageData(x=30, y=40, width=80, height=60, rotation=90, z_index=3) AlignmentManager.align_left([elem1, elem2]) # Rotation and z_index should not change assert elem1.rotation == 45 assert elem1.z_index == 5 assert elem2.rotation == 90 assert elem2.z_index == 3 # Heights should not change assert elem1.size[1] == 50 assert elem2.size[1] == 60 def test_distribute_with_unsorted_elements(self): """Test distribution works correctly with unsorted input""" # Create elements in random order elem3 = ImageData(x=200, y=60, width=90, height=40) elem1 = ImageData(x=0, y=20, width=100, height=50) elem2 = ImageData(x=100, y=40, width=80, height=60) # Pass in random order changes = AlignmentManager.distribute_horizontally([elem3, elem1, elem2]) # Should still distribute correctly positions = sorted([elem1.position[0], elem2.position[0], elem3.position[0]]) assert positions[0] == 0 assert positions[1] == 100 assert positions[2] == 200 def test_space_with_varying_sizes(self): """Test spacing works correctly with elements of varying sizes""" elem1 = ImageData(x=0, y=0, width=50, height=50) elem2 = ImageData(x=100, y=0, width=100, height=50) elem3 = ImageData(x=250, y=0, width=75, height=50) changes = AlignmentManager.space_horizontally([elem1, elem2, elem3]) # Calculate expected spacing # Total width = 50 + 100 + 75 = 225 # rightmost edge = 250 + 75 = 325 # Available space = 325 - 0 - 225 = 100 # Spacing = 100 / 2 = 50 sorted_elements = sorted([elem1, elem2, elem3], key=lambda e: e.position[0]) # Verify spacing between elements is equal gap1 = sorted_elements[1].position[0] - (sorted_elements[0].position[0] + sorted_elements[0].size[0]) gap2 = sorted_elements[2].position[0] - (sorted_elements[1].position[0] + sorted_elements[1].size[0]) assert abs(gap1 - 50) < 0.01 assert abs(gap2 - 50) < 0.01 def test_maximize_pattern_empty_list(self): """Test maximize_pattern with empty list""" changes = AlignmentManager.maximize_pattern([], (297, 210)) assert changes == [] def test_maximize_pattern_single_element(self): """Test maximize_pattern with single element""" # Small element in the middle of the page elem = ImageData(x=100, y=80, width=20, height=15) page_size = (297, 210) # A4 landscape in mm changes = AlignmentManager.maximize_pattern([elem], page_size, min_gap=2.0) # Element should grow significantly assert elem.size[0] > 20 assert elem.size[1] > 15 # Should maintain aspect ratio original_aspect = 20 / 15 new_aspect = elem.size[0] / elem.size[1] assert abs(original_aspect - new_aspect) < 0.01 # Should not exceed page boundaries (with min_gap) assert elem.position[0] >= 2.0 assert elem.position[1] >= 2.0 assert elem.position[0] + elem.size[0] <= 297 - 2.0 assert elem.position[1] + elem.size[1] <= 210 - 2.0 # Check undo information assert len(changes) == 1 assert changes[0][0] == elem assert changes[0][1] == (100, 80) # old position assert changes[0][2] == (20, 15) # old size def test_maximize_pattern_two_elements_horizontal(self): """Test maximize_pattern with two elements side by side""" elem1 = ImageData(x=50, y=80, width=20, height=20) elem2 = ImageData(x=200, y=80, width=20, height=20) page_size = (297, 210) # A4 landscape in mm changes = AlignmentManager.maximize_pattern([elem1, elem2], page_size, min_gap=2.0) # Both elements should grow assert elem1.size[0] > 20 and elem1.size[1] > 20 assert elem2.size[0] > 20 and elem2.size[1] > 20 # Elements should not overlap (min_gap = 2.0) gap_x = max( elem2.position[0] - (elem1.position[0] + elem1.size[0]), elem1.position[0] - (elem2.position[0] + elem2.size[0]) ) gap_y = max( elem2.position[1] - (elem1.position[1] + elem1.size[1]), elem1.position[1] - (elem2.position[1] + elem2.size[1]) ) # Either horizontal or vertical gap should be >= min_gap assert gap_x >= 2.0 or gap_y >= 2.0 # Both elements should respect page boundaries for elem in [elem1, elem2]: assert elem.position[0] >= 2.0 assert elem.position[1] >= 2.0 assert elem.position[0] + elem.size[0] <= 297 - 2.0 assert elem.position[1] + elem.size[1] <= 210 - 2.0 def test_maximize_pattern_three_elements_grid(self): """Test maximize_pattern with three elements in a grid pattern""" elem1 = ImageData(x=50, y=50, width=15, height=15) elem2 = ImageData(x=150, y=50, width=15, height=15) elem3 = ImageData(x=100, y=120, width=15, height=15) page_size = (297, 210) # A4 landscape in mm changes = AlignmentManager.maximize_pattern([elem1, elem2, elem3], page_size, min_gap=2.0) # All elements should grow for elem in [elem1, elem2, elem3]: assert elem.size[0] > 15 assert elem.size[1] > 15 # Check no overlaps with min_gap elements = [elem1, elem2, elem3] for i, elem_a in enumerate(elements): for j, elem_b in enumerate(elements): if i >= j: continue # Calculate gaps between rectangles gap_x = max( elem_b.position[0] - (elem_a.position[0] + elem_a.size[0]), elem_a.position[0] - (elem_b.position[0] + elem_b.size[0]) ) gap_y = max( elem_b.position[1] - (elem_a.position[1] + elem_a.size[1]), elem_a.position[1] - (elem_b.position[1] + elem_b.size[1]) ) # At least one gap should be >= min_gap assert gap_x >= 2.0 or gap_y >= 2.0 # Check undo information assert len(changes) == 3 def test_maximize_pattern_respects_boundaries(self): """Test that maximize_pattern respects page boundaries""" elem = ImageData(x=10, y=10, width=10, height=10) page_size = (100, 100) min_gap = 5.0 changes = AlignmentManager.maximize_pattern([elem], page_size, min_gap=min_gap) # Element should not exceed boundaries assert elem.position[0] >= min_gap assert elem.position[1] >= min_gap assert elem.position[0] + elem.size[0] <= page_size[0] - min_gap assert elem.position[1] + elem.size[1] <= page_size[1] - min_gap def test_maximize_pattern_maintains_aspect_ratio(self): """Test that maximize_pattern maintains element aspect ratios""" elem1 = ImageData(x=50, y=50, width=30, height=20) # 3:2 aspect elem2 = ImageData(x=150, y=50, width=20, height=30) # 2:3 aspect page_size = (297, 210) original_aspect1 = elem1.size[0] / elem1.size[1] original_aspect2 = elem2.size[0] / elem2.size[1] changes = AlignmentManager.maximize_pattern([elem1, elem2], page_size, min_gap=2.0) # Aspect ratios should be maintained new_aspect1 = elem1.size[0] / elem1.size[1] new_aspect2 = elem2.size[0] / elem2.size[1] assert abs(original_aspect1 - new_aspect1) < 0.01 assert abs(original_aspect2 - new_aspect2) < 0.01 def test_maximize_pattern_with_constrained_space(self): """Test maximize_pattern when elements are tightly packed""" # Create 4 elements in corners with limited space elem1 = ImageData(x=10, y=10, width=10, height=10) elem2 = ImageData(x=140, y=10, width=10, height=10) elem3 = ImageData(x=10, y=90, width=10, height=10) elem4 = ImageData(x=140, y=90, width=10, height=10) page_size = (160, 110) changes = AlignmentManager.maximize_pattern( [elem1, elem2, elem3, elem4], page_size, min_gap=2.0 ) # All elements should grow for elem in [elem1, elem2, elem3, elem4]: assert elem.size[0] > 10 assert elem.size[1] > 10 # Verify no overlaps elements = [elem1, elem2, elem3, elem4] for i, elem_a in enumerate(elements): for j, elem_b in enumerate(elements): if i >= j: continue gap_x = max( elem_b.position[0] - (elem_a.position[0] + elem_a.size[0]), elem_a.position[0] - (elem_b.position[0] + elem_b.size[0]) ) gap_y = max( elem_b.position[1] - (elem_a.position[1] + elem_a.size[1]), elem_a.position[1] - (elem_b.position[1] + elem_b.size[1]) ) assert gap_x >= 2.0 or gap_y >= 2.0 def test_maximize_pattern_with_different_element_types(self): """Test maximize_pattern works with different element types""" elem1 = ImageData(x=50, y=50, width=20, height=20) elem2 = PlaceholderData(placeholder_type="image", x=150, y=50, width=20, height=20) elem3 = TextBoxData(text_content="Test", x=100, y=120, width=20, height=20) page_size = (297, 210) changes = AlignmentManager.maximize_pattern([elem1, elem2, elem3], page_size, min_gap=2.0) # All elements should grow assert elem1.size[0] > 20 assert elem2.size[0] > 20 assert elem3.size[0] > 20 # Check undo information has correct element types assert isinstance(changes[0][0], ImageData) assert isinstance(changes[1][0], PlaceholderData) assert isinstance(changes[2][0], TextBoxData)