497 lines
17 KiB
Python
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
|