pyPhotoAlbum/tests/test_snapping.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

512 lines
18 KiB
Python
Executable File

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