""" Unit tests for pyPhotoAlbum snapping system """ import pytest from pyPhotoAlbum.snapping import SnappingSystem, Guide class TestGuide: """Tests for Guide class""" def test_guide_initialization(self): """Test Guide initialization""" guide = Guide(position=50.0, orientation='vertical') assert guide.position == 50.0 assert guide.orientation == 'vertical' def test_guide_serialization(self): """Test Guide serialization to dictionary""" guide = Guide(position=75.5, orientation='horizontal') data = guide.serialize() assert data['position'] == 75.5 assert data['orientation'] == 'horizontal' def test_guide_deserialization(self): """Test Guide deserialization from dictionary""" data = {'position': 100.0, 'orientation': 'vertical'} guide = Guide.deserialize(data) assert guide.position == 100.0 assert guide.orientation == 'vertical' def test_guide_deserialization_with_defaults(self): """Test Guide deserialization with missing fields uses defaults""" data = {} guide = Guide.deserialize(data) assert guide.position == 0 assert guide.orientation == 'vertical' class TestSnappingSystem: """Tests for SnappingSystem class""" def test_initialization_default(self): """Test SnappingSystem initialization with default values""" system = SnappingSystem() assert system.snap_threshold_mm == 5.0 assert system.grid_size_mm == 10.0 assert system.snap_to_grid == False assert system.snap_to_edges == True assert system.snap_to_guides == True assert len(system.guides) == 0 def test_initialization_with_threshold(self): """Test SnappingSystem initialization with custom threshold""" system = SnappingSystem(snap_threshold_mm=3.0) assert system.snap_threshold_mm == 3.0 def test_add_guide(self): """Test adding a guide""" system = SnappingSystem() guide = system.add_guide(position=50.0, orientation='vertical') assert len(system.guides) == 1 assert guide.position == 50.0 assert guide.orientation == 'vertical' assert guide in system.guides def test_add_multiple_guides(self): """Test adding multiple guides""" system = SnappingSystem() guide1 = system.add_guide(position=50.0, orientation='vertical') guide2 = system.add_guide(position=100.0, orientation='horizontal') guide3 = system.add_guide(position=150.0, orientation='vertical') assert len(system.guides) == 3 assert guide1 in system.guides assert guide2 in system.guides assert guide3 in system.guides def test_remove_guide(self): """Test removing a guide""" system = SnappingSystem() guide = system.add_guide(position=50.0, orientation='vertical') system.remove_guide(guide) assert len(system.guides) == 0 assert guide not in system.guides def test_remove_guide_not_in_list(self): """Test removing a guide that's not in the list does nothing""" system = SnappingSystem() guide1 = system.add_guide(position=50.0, orientation='vertical') guide2 = Guide(position=100.0, orientation='horizontal') # Should not raise an error system.remove_guide(guide2) assert len(system.guides) == 1 assert guide1 in system.guides def test_clear_guides(self): """Test clearing all guides""" system = SnappingSystem() system.add_guide(position=50.0, orientation='vertical') system.add_guide(position=100.0, orientation='horizontal') system.add_guide(position=150.0, orientation='vertical') system.clear_guides() assert len(system.guides) == 0 def test_snap_position_no_snapping_enabled(self): """Test snap_position with all snapping disabled""" system = SnappingSystem() system.snap_to_grid = False system.snap_to_edges = False system.snap_to_guides = False position = (25.0, 35.0) size = (100.0, 100.0) page_size = (210.0, 297.0) snapped = system.snap_position(position, size, page_size, dpi=300) assert snapped == position # Should not snap def test_snap_position_to_edges(self): """Test snap_position snapping to page edges""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_grid = False system.snap_to_edges = True system.snap_to_guides = False # Position near left edge (should snap to 0) position = (10.0, 50.0) # Close to 0 in pixels size = (100.0, 100.0) page_size = (210.0, 297.0) # A4 size in mm snapped = system.snap_position(position, size, page_size, dpi=300) assert snapped[0] == 0 # Should snap to left edge def test_snap_position_to_grid(self): """Test snap_position snapping to grid""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_grid = True system.snap_to_edges = False system.snap_to_guides = False system.grid_size_mm = 10.0 # Position near a grid line dpi = 300 grid_size_px = 10.0 * dpi / 25.4 # ~118 pixels position = (grid_size_px + 5, grid_size_px + 5) # Close to a grid point size = (100.0, 100.0) page_size = (210.0, 297.0) snapped = system.snap_position(position, size, page_size, dpi=dpi) # Should snap to nearest grid line assert abs(snapped[0] - grid_size_px) < 1 # Allow small floating point error assert abs(snapped[1] - grid_size_px) < 1 def test_snap_position_to_guides(self): """Test snap_position snapping to guides""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_grid = False system.snap_to_edges = False system.snap_to_guides = True dpi = 300 guide_pos_mm = 50.0 guide_pos_px = guide_pos_mm * dpi / 25.4 system.add_guide(position=guide_pos_mm, orientation='vertical') system.add_guide(position=guide_pos_mm, orientation='horizontal') # Position near the guides position = (guide_pos_px + 5, guide_pos_px + 5) size = (100.0, 100.0) page_size = (210.0, 297.0) snapped = system.snap_position(position, size, page_size, dpi=dpi) # Should snap to guides assert abs(snapped[0] - guide_pos_px) < 1 assert abs(snapped[1] - guide_pos_px) < 1 def test_snap_position_outside_threshold(self): """Test snap_position when position is outside snap threshold""" system = SnappingSystem(snap_threshold_mm=2.0) # Small threshold system.snap_to_edges = True # Position far from edges position = (500.0, 600.0) size = (100.0, 100.0) page_size = (210.0, 297.0) snapped = system.snap_position(position, size, page_size, dpi=300) assert snapped == position # Should not snap when too far def test_snap_resize_bottom_right_handle(self): """Test snap_resize with bottom-right handle""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_grid = True system.grid_size_mm = 10.0 position = (100.0, 100.0) size = (200.0, 200.0) dx = 10.0 dy = 10.0 resize_handle = 'se' page_size = (210.0, 297.0) new_pos, new_size = system.snap_resize( position, size, dx, dy, resize_handle, page_size, dpi=300 ) # Position shouldn't change for bottom-right handle assert new_pos == position # Size should change assert new_size[0] > size[0] assert new_size[1] > size[1] def test_snap_resize_top_left_handle(self): """Test snap_resize with top-left handle""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_edges = True position = (150.0, 150.0) size = (200.0, 200.0) dx = -10.0 dy = -10.0 resize_handle = 'nw' page_size = (210.0, 297.0) new_pos, new_size = system.snap_resize( position, size, dx, dy, resize_handle, page_size, dpi=300 ) # Both position and size should change for top-left handle assert new_pos != position assert new_size != size def test_snap_resize_top_handle(self): """Test snap_resize with top handle only""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_edges = True position = (100.0, 100.0) size = (200.0, 200.0) dx = 0.0 dy = -10.0 resize_handle = 'n' page_size = (210.0, 297.0) new_pos, new_size = system.snap_resize( position, size, dx, dy, resize_handle, page_size, dpi=300 ) # X position should stay same, Y should change assert new_pos[0] == position[0] assert new_pos[1] != position[1] # Width should stay same, height should change assert new_size[0] == size[0] assert new_size[1] != size[1] def test_snap_resize_right_handle(self): """Test snap_resize with right handle only""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_edges = True position = (100.0, 100.0) size = (200.0, 200.0) dx = 10.0 dy = 0.0 resize_handle = 'e' page_size = (210.0, 297.0) new_pos, new_size = system.snap_resize( position, size, dx, dy, resize_handle, page_size, dpi=300 ) # Position should stay same assert new_pos == position # Width should change, height should stay same assert new_size[0] != size[0] assert new_size[1] == size[1] def test_snap_resize_minimum_size(self): """Test snap_resize enforces minimum size""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_edges = False position = (100.0, 100.0) size = (50.0, 50.0) dx = -100.0 # Try to make it very small dy = -100.0 resize_handle = 'se' page_size = (210.0, 297.0) new_pos, new_size = system.snap_resize( position, size, dx, dy, resize_handle, page_size, dpi=300 ) # Should enforce minimum size of 10 pixels assert new_size[0] >= 10 assert new_size[1] >= 10 def test_snap_resize_all_handles(self): """Test snap_resize works with all handle types""" system = SnappingSystem(snap_threshold_mm=5.0) system.snap_to_edges = False position = (100.0, 100.0) size = (200.0, 200.0) dx = 10.0 dy = 10.0 page_size = (210.0, 297.0) handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'] for handle in handles: new_pos, new_size = system.snap_resize( position, size, dx, dy, handle, page_size, dpi=300 ) # Should return valid position and size assert isinstance(new_pos, tuple) assert len(new_pos) == 2 assert isinstance(new_size, tuple) assert len(new_size) == 2 assert new_size[0] >= 10 # Minimum size assert new_size[1] >= 10 def test_get_snap_lines_empty(self): """Test get_snap_lines with no snapping enabled""" system = SnappingSystem() system.snap_to_grid = False system.snap_to_edges = False system.snap_to_guides = False page_size = (210.0, 297.0) lines = system.get_snap_lines(page_size, dpi=300) assert lines['grid'] == [] assert lines['edges'] == [] assert lines['guides'] == [] def test_get_snap_lines_with_grid(self): """Test get_snap_lines with grid enabled""" system = SnappingSystem() system.snap_to_grid = True system.grid_size_mm = 10.0 page_size = (30.0, 30.0) # Small page for easier testing lines = system.get_snap_lines(page_size, dpi=300) # Should have grid lines assert len(lines['grid']) > 0 # Should have both vertical and horizontal grid lines vertical_lines = [line for line in lines['grid'] if line[0] == 'vertical'] horizontal_lines = [line for line in lines['grid'] if line[0] == 'horizontal'] assert len(vertical_lines) > 0 assert len(horizontal_lines) > 0 def test_get_snap_lines_with_edges(self): """Test get_snap_lines with edge snapping enabled""" system = SnappingSystem() system.snap_to_edges = True page_size = (210.0, 297.0) lines = system.get_snap_lines(page_size, dpi=300) # Should have exactly 4 edge lines (left, right, top, bottom) assert len(lines['edges']) == 4 # Check for vertical edges vertical_edges = [line for line in lines['edges'] if line[0] == 'vertical'] assert len(vertical_edges) == 2 # Check for horizontal edges horizontal_edges = [line for line in lines['edges'] if line[0] == 'horizontal'] assert len(horizontal_edges) == 2 def test_get_snap_lines_with_guides(self): """Test get_snap_lines with guides""" system = SnappingSystem() system.snap_to_guides = True system.add_guide(position=50.0, orientation='vertical') system.add_guide(position=100.0, orientation='horizontal') system.add_guide(position=150.0, orientation='vertical') page_size = (210.0, 297.0) lines = system.get_snap_lines(page_size, dpi=300) # Should have guide lines assert len(lines['guides']) == 3 # Check orientations vertical_guides = [line for line in lines['guides'] if line[0] == 'vertical'] horizontal_guides = [line for line in lines['guides'] if line[0] == 'horizontal'] assert len(vertical_guides) == 2 assert len(horizontal_guides) == 1 def test_serialization(self): """Test SnappingSystem serialization to dictionary""" system = SnappingSystem(snap_threshold_mm=3.0) system.grid_size_mm = 15.0 system.snap_to_grid = True system.snap_to_edges = False system.snap_to_guides = True system.add_guide(position=50.0, orientation='vertical') system.add_guide(position=100.0, orientation='horizontal') data = system.serialize() assert data['snap_threshold_mm'] == 3.0 assert data['grid_size_mm'] == 15.0 assert data['snap_to_grid'] == True assert data['snap_to_edges'] == False assert data['snap_to_guides'] == True assert len(data['guides']) == 2 def test_deserialization(self): """Test SnappingSystem deserialization from dictionary""" system = SnappingSystem() data = { 'snap_threshold_mm': 4.0, 'grid_size_mm': 20.0, 'snap_to_grid': True, 'snap_to_edges': False, 'snap_to_guides': True, 'guides': [ {'position': 50.0, 'orientation': 'vertical'}, {'position': 100.0, 'orientation': 'horizontal'} ] } system.deserialize(data) assert system.snap_threshold_mm == 4.0 assert system.grid_size_mm == 20.0 assert system.snap_to_grid == True assert system.snap_to_edges == False assert system.snap_to_guides == True assert len(system.guides) == 2 assert system.guides[0].position == 50.0 assert system.guides[0].orientation == 'vertical' assert system.guides[1].position == 100.0 assert system.guides[1].orientation == 'horizontal' def test_deserialization_with_defaults(self): """Test SnappingSystem deserialization with missing fields uses defaults""" system = SnappingSystem() data = {} system.deserialize(data) assert system.snap_threshold_mm == 5.0 assert system.grid_size_mm == 10.0 assert system.snap_to_grid == False assert system.snap_to_edges == True assert system.snap_to_guides == True assert len(system.guides) == 0 def test_serialize_deserialize_roundtrip(self): """Test that serialize and deserialize are inverse operations""" original = SnappingSystem(snap_threshold_mm=7.5) original.grid_size_mm = 12.5 original.snap_to_grid = True original.snap_to_edges = True original.snap_to_guides = False original.add_guide(position=25.5, orientation='vertical') original.add_guide(position=75.5, orientation='horizontal') original.add_guide(position=125.5, orientation='vertical') data = original.serialize() restored = SnappingSystem() restored.deserialize(data) assert restored.snap_threshold_mm == original.snap_threshold_mm assert restored.grid_size_mm == original.grid_size_mm assert restored.snap_to_grid == original.snap_to_grid assert restored.snap_to_edges == original.snap_to_edges assert restored.snap_to_guides == original.snap_to_guides assert len(restored.guides) == len(original.guides) for orig_guide, rest_guide in zip(original.guides, restored.guides): assert rest_guide.position == orig_guide.position assert rest_guide.orientation == orig_guide.orientation