pyPhotoAlbum/tests/test_snapping.py
Duncan Tourolle 795c0c531c
All checks were successful
Python CI / test (push) Successful in 57s
Lint / lint (push) Successful in 1m6s
Tests / test (3.10) (push) Successful in 43s
Tests / test (3.11) (push) Successful in 45s
Tests / test (3.9) (push) Successful in 43s
Improved snapping
2025-10-28 21:37:39 +01:00

497 lines
17 KiB
Python

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