""" 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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) params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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) params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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) params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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) params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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) params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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""" from pyPhotoAlbum.snapping import SnapResizeParams 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: params = SnapResizeParams( position=position, size=size, dx=dx, dy=dy, resize_handle=handle, page_size=page_size, dpi=300 ) new_pos, new_size = system.snap_resize(params) # 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