All checks were successful
Python CI / test (push) Successful in 1m41s
Lint / lint (push) Successful in 1m29s
Tests / test (3.11) (push) Successful in 1m46s
Tests / test (3.12) (push) Successful in 1m50s
Tests / test (3.13) (push) Successful in 1m43s
Tests / test (3.14) (push) Successful in 1m26s
586 lines
18 KiB
Python
586 lines
18 KiB
Python
"""
|
|
Comprehensive tests for SnappingSystem
|
|
"""
|
|
|
|
import pytest
|
|
import math
|
|
from unittest.mock import Mock
|
|
|
|
from pyPhotoAlbum.snapping import SnappingSystem, Guide, SnapResizeParams
|
|
|
|
|
|
class TestGuide:
|
|
"""Tests for Guide dataclass"""
|
|
|
|
def test_guide_creation(self):
|
|
"""Test creating a Guide"""
|
|
guide = Guide(position=100.0, orientation="vertical")
|
|
assert guide.position == 100.0
|
|
assert guide.orientation == "vertical"
|
|
|
|
def test_guide_serialize(self):
|
|
"""Test Guide serialization"""
|
|
guide = Guide(position=50.5, orientation="horizontal")
|
|
data = guide.serialize()
|
|
|
|
assert data["position"] == 50.5
|
|
assert data["orientation"] == "horizontal"
|
|
|
|
def test_guide_deserialize(self):
|
|
"""Test Guide deserialization"""
|
|
data = {"position": 75.0, "orientation": "vertical"}
|
|
guide = Guide.deserialize(data)
|
|
|
|
assert guide.position == 75.0
|
|
assert guide.orientation == "vertical"
|
|
|
|
def test_guide_deserialize_defaults(self):
|
|
"""Test Guide deserialization with missing fields"""
|
|
guide = Guide.deserialize({})
|
|
|
|
assert guide.position == 0
|
|
assert guide.orientation == "vertical"
|
|
|
|
|
|
class TestSnappingSystemInit:
|
|
"""Tests for SnappingSystem initialization"""
|
|
|
|
def test_default_init(self):
|
|
"""Test default initialization"""
|
|
snap = SnappingSystem()
|
|
|
|
assert snap.snap_threshold_mm == 5.0
|
|
assert snap.grid_size_mm == 10.0
|
|
assert snap.snap_to_grid is False
|
|
assert snap.snap_to_edges is True
|
|
assert snap.snap_to_guides is True
|
|
assert snap.guides == []
|
|
|
|
def test_custom_threshold(self):
|
|
"""Test initialization with custom threshold"""
|
|
snap = SnappingSystem(snap_threshold_mm=10.0)
|
|
assert snap.snap_threshold_mm == 10.0
|
|
|
|
|
|
class TestGuideManagement:
|
|
"""Tests for guide management methods"""
|
|
|
|
def test_add_guide(self):
|
|
"""Test adding a guide"""
|
|
snap = SnappingSystem()
|
|
guide = snap.add_guide(100.0, "vertical")
|
|
|
|
assert len(snap.guides) == 1
|
|
assert snap.guides[0].position == 100.0
|
|
assert snap.guides[0].orientation == "vertical"
|
|
|
|
def test_add_multiple_guides(self):
|
|
"""Test adding multiple guides"""
|
|
snap = SnappingSystem()
|
|
snap.add_guide(50.0, "horizontal")
|
|
snap.add_guide(100.0, "vertical")
|
|
snap.add_guide(150.0, "horizontal")
|
|
|
|
assert len(snap.guides) == 3
|
|
|
|
def test_remove_guide(self):
|
|
"""Test removing a guide"""
|
|
snap = SnappingSystem()
|
|
guide = snap.add_guide(100.0, "vertical")
|
|
|
|
snap.remove_guide(guide)
|
|
|
|
assert len(snap.guides) == 0
|
|
|
|
def test_remove_nonexistent_guide(self):
|
|
"""Test removing a guide that doesn't exist"""
|
|
snap = SnappingSystem()
|
|
guide = Guide(position=100.0, orientation="vertical")
|
|
|
|
# Should not raise exception
|
|
snap.remove_guide(guide)
|
|
assert len(snap.guides) == 0
|
|
|
|
def test_clear_guides(self):
|
|
"""Test clearing all guides"""
|
|
snap = SnappingSystem()
|
|
snap.add_guide(50.0, "horizontal")
|
|
snap.add_guide(100.0, "vertical")
|
|
snap.add_guide(150.0, "horizontal")
|
|
|
|
snap.clear_guides()
|
|
|
|
assert len(snap.guides) == 0
|
|
|
|
|
|
class TestSnapPosition:
|
|
"""Tests for snap_position method"""
|
|
|
|
def test_no_snapping_when_disabled(self):
|
|
"""Test that no snapping occurs when all snapping is disabled"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_guides = False
|
|
|
|
position = (100, 100)
|
|
size = (50, 50)
|
|
page_size = (210, 297) # A4 in mm
|
|
|
|
result = snap.snap_position(position, size, page_size)
|
|
|
|
assert result == position
|
|
|
|
def test_snap_to_left_edge(self):
|
|
"""Test snapping to left page edge"""
|
|
snap = SnappingSystem(snap_threshold_mm=10.0)
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
# Position close to left edge (within threshold)
|
|
# At 300 DPI, 10mm = ~118 pixels
|
|
position = (5, 100) # Very close to left edge (0)
|
|
size = (50, 50)
|
|
page_size = (210, 297)
|
|
|
|
result = snap.snap_position(position, size, page_size)
|
|
|
|
# Should snap to 0 (left edge)
|
|
assert result[0] == 0
|
|
|
|
def test_snap_to_right_edge(self):
|
|
"""Test snapping to right page edge"""
|
|
snap = SnappingSystem(snap_threshold_mm=10.0)
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
dpi = 300
|
|
page_width_mm = 210
|
|
page_width_px = page_width_mm * dpi / 25.4
|
|
element_width = 50
|
|
|
|
# Position close to right edge
|
|
position = (page_width_px - element_width - 5, 100)
|
|
size = (element_width, 50)
|
|
page_size = (page_width_mm, 297)
|
|
|
|
result = snap.snap_position(position, size, page_size, dpi)
|
|
|
|
# Should snap so element's right edge aligns with page right edge
|
|
expected_x = page_width_px - element_width
|
|
assert abs(result[0] - expected_x) < 1
|
|
|
|
def test_snap_to_grid(self):
|
|
"""Test snapping to grid"""
|
|
snap = SnappingSystem(snap_threshold_mm=5.0)
|
|
snap.snap_to_grid = True
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_guides = False
|
|
snap.grid_size_mm = 10.0
|
|
|
|
dpi = 300
|
|
grid_size_px = 10.0 * dpi / 25.4 # ~118 pixels
|
|
|
|
# Position slightly off grid
|
|
position = (grid_size_px + 10, grid_size_px + 10)
|
|
size = (50, 50)
|
|
page_size = (210, 297)
|
|
|
|
result = snap.snap_position(position, size, page_size, dpi)
|
|
|
|
# Should snap to nearest grid intersection
|
|
assert abs(result[0] - grid_size_px) < 1 or abs(result[0] - 2 * grid_size_px) < 1
|
|
|
|
def test_snap_to_guides(self):
|
|
"""Test snapping to guides"""
|
|
snap = SnappingSystem(snap_threshold_mm=10.0)
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = True
|
|
|
|
dpi = 300
|
|
guide_pos_mm = 50.0
|
|
guide_pos_px = guide_pos_mm * dpi / 25.4
|
|
|
|
snap.add_guide(guide_pos_mm, "vertical")
|
|
snap.add_guide(guide_pos_mm, "horizontal")
|
|
|
|
# Position close to guide intersection
|
|
position = (guide_pos_px + 5, guide_pos_px + 5)
|
|
size = (50, 50)
|
|
page_size = (210, 297)
|
|
|
|
result = snap.snap_position(position, size, page_size, dpi)
|
|
|
|
# Should snap to guide intersection
|
|
assert abs(result[0] - guide_pos_px) < 1 or abs(result[1] - guide_pos_px) < 1
|
|
|
|
def test_snap_uses_euclidean_distance(self):
|
|
"""Test that snapping uses Euclidean distance for point selection"""
|
|
snap = SnappingSystem(snap_threshold_mm=20.0)
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
# Position close to origin - should snap to (0, 0)
|
|
# At 300 DPI, 20mm threshold = ~236 pixels
|
|
# Position (50, 50) has euclidean distance ~70.7 from (0, 0)
|
|
# which is well within the threshold
|
|
position = (50, 50)
|
|
size = (50, 50)
|
|
page_size = (210, 297)
|
|
dpi = 300
|
|
|
|
result = snap.snap_position(position, size, page_size, dpi)
|
|
|
|
# Should snap to (0, 0) corner as it's closest and within threshold
|
|
# Note: snap_position considers multiple snap points; check we got one of them
|
|
assert result[0] == 0 or result[1] == 0, f"Expected at least one axis to snap to 0, got {result}"
|
|
|
|
def test_snap_with_project_settings(self):
|
|
"""Test snapping with project settings override"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_grid = False # Local setting
|
|
|
|
mock_project = Mock()
|
|
mock_project.snap_to_grid = True
|
|
mock_project.snap_to_edges = False
|
|
mock_project.snap_to_guides = False
|
|
mock_project.grid_size_mm = 10.0
|
|
mock_project.snap_threshold_mm = 5.0
|
|
|
|
dpi = 300
|
|
grid_size_px = 10.0 * dpi / 25.4
|
|
|
|
# Position near grid line
|
|
position = (grid_size_px + 5, grid_size_px + 5)
|
|
size = (50, 50)
|
|
page_size = (210, 297)
|
|
|
|
result = snap.snap_position(position, size, page_size, dpi, mock_project)
|
|
|
|
# Should use project settings and snap to grid
|
|
# The result should be different from input (snapped)
|
|
assert result != position
|
|
|
|
|
|
class TestSnapResize:
|
|
"""Tests for snap_resize method"""
|
|
|
|
def test_resize_southeast_handle(self):
|
|
"""Test resizing from SE corner"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
params = SnapResizeParams(
|
|
position=(100, 100), size=(100, 100), dx=50, dy=50, resize_handle="se", page_size=(210, 297)
|
|
)
|
|
|
|
new_pos, new_size = snap.snap_resize(params)
|
|
|
|
# Position should stay same for SE resize
|
|
assert new_pos == (100, 100)
|
|
# Size should increase
|
|
assert new_size == (150, 150)
|
|
|
|
def test_resize_northwest_handle(self):
|
|
"""Test resizing from NW corner"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
params = SnapResizeParams(
|
|
position=(100, 100), size=(100, 100), dx=-20, dy=-20, resize_handle="nw", page_size=(210, 297)
|
|
)
|
|
|
|
new_pos, new_size = snap.snap_resize(params)
|
|
|
|
# Position should move for NW resize
|
|
assert new_pos == (80, 80)
|
|
# Size should increase
|
|
assert new_size == (120, 120)
|
|
|
|
def test_resize_minimum_size(self):
|
|
"""Test that resize enforces minimum size"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
params = SnapResizeParams(
|
|
position=(100, 100),
|
|
size=(50, 50),
|
|
dx=-100, # Would make width negative
|
|
dy=-100, # Would make height negative
|
|
resize_handle="se",
|
|
page_size=(210, 297),
|
|
)
|
|
|
|
new_pos, new_size = snap.snap_resize(params)
|
|
|
|
# Size should be clamped to minimum
|
|
assert new_size[0] >= 10
|
|
assert new_size[1] >= 10
|
|
|
|
def test_resize_snap_to_edge(self):
|
|
"""Test that resize snaps edges to page boundaries"""
|
|
snap = SnappingSystem(snap_threshold_mm=10.0)
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
dpi = 300
|
|
page_width_px = 210 * dpi / 25.4
|
|
|
|
params = SnapResizeParams(
|
|
position=(100, 100),
|
|
size=(100, 100),
|
|
dx=page_width_px - 200 - 5, # Almost to right edge
|
|
dy=0,
|
|
resize_handle="e",
|
|
page_size=(210, 297),
|
|
dpi=dpi,
|
|
)
|
|
|
|
new_pos, new_size = snap.snap_resize(params)
|
|
|
|
# Right edge should snap to page edge
|
|
right_edge = new_pos[0] + new_size[0]
|
|
assert abs(right_edge - page_width_px) < 20 # Within snap threshold
|
|
|
|
|
|
class TestSnapEdgeToTargets:
|
|
"""Tests for _snap_edge_to_targets method"""
|
|
|
|
def test_snap_to_page_start_edge(self):
|
|
"""Test snapping to page start edge (0)"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
dpi = 300
|
|
threshold_px = 50
|
|
|
|
result = snap._snap_edge_to_targets(
|
|
edge_position=10, page_size_mm=210, dpi=dpi, snap_threshold_px=threshold_px, orientation="vertical"
|
|
)
|
|
|
|
assert result == 0
|
|
|
|
def test_snap_to_page_end_edge(self):
|
|
"""Test snapping to page end edge"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
dpi = 300
|
|
page_size_mm = 210
|
|
page_size_px = page_size_mm * dpi / 25.4
|
|
threshold_px = 50
|
|
|
|
result = snap._snap_edge_to_targets(
|
|
edge_position=page_size_px - 10,
|
|
page_size_mm=page_size_mm,
|
|
dpi=dpi,
|
|
snap_threshold_px=threshold_px,
|
|
orientation="vertical",
|
|
)
|
|
|
|
assert result == page_size_px
|
|
|
|
def test_snap_to_grid_line(self):
|
|
"""Test snapping to grid line"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = True
|
|
snap.snap_to_guides = False
|
|
snap.grid_size_mm = 10.0
|
|
|
|
dpi = 300
|
|
grid_size_px = 10.0 * dpi / 25.4
|
|
threshold_px = 50
|
|
|
|
result = snap._snap_edge_to_targets(
|
|
edge_position=grid_size_px + 5,
|
|
page_size_mm=210,
|
|
dpi=dpi,
|
|
snap_threshold_px=threshold_px,
|
|
orientation="vertical",
|
|
)
|
|
|
|
assert result == grid_size_px
|
|
|
|
def test_snap_to_guide(self):
|
|
"""Test snapping to guide"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = True
|
|
|
|
guide_pos_mm = 50.0
|
|
snap.add_guide(guide_pos_mm, "vertical")
|
|
|
|
dpi = 300
|
|
guide_pos_px = guide_pos_mm * dpi / 25.4
|
|
threshold_px = 50
|
|
|
|
result = snap._snap_edge_to_targets(
|
|
edge_position=guide_pos_px + 5,
|
|
page_size_mm=210,
|
|
dpi=dpi,
|
|
snap_threshold_px=threshold_px,
|
|
orientation="vertical",
|
|
)
|
|
|
|
assert result == guide_pos_px
|
|
|
|
def test_no_snap_when_out_of_threshold(self):
|
|
"""Test no snap when edge is outside threshold"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
dpi = 300
|
|
threshold_px = 10
|
|
|
|
result = snap._snap_edge_to_targets(
|
|
edge_position=500, # Far from any edge
|
|
page_size_mm=210,
|
|
dpi=dpi,
|
|
snap_threshold_px=threshold_px,
|
|
orientation="vertical",
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestGetSnapLines:
|
|
"""Tests for get_snap_lines method"""
|
|
|
|
def test_get_snap_lines_edges_only(self):
|
|
"""Test getting snap lines with edges only"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = True
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = False
|
|
|
|
result = snap.get_snap_lines((210, 297))
|
|
|
|
assert len(result["edges"]) == 4 # 4 edges
|
|
assert len(result["grid"]) == 0
|
|
assert len(result["guides"]) == 0
|
|
|
|
def test_get_snap_lines_with_grid(self):
|
|
"""Test getting snap lines with grid enabled"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = True
|
|
snap.snap_to_guides = False
|
|
snap.grid_size_mm = 10.0
|
|
|
|
result = snap.get_snap_lines((100, 100), dpi=300)
|
|
|
|
# Should have multiple grid lines
|
|
assert len(result["grid"]) > 0
|
|
assert len(result["edges"]) == 0
|
|
|
|
def test_get_snap_lines_with_guides(self):
|
|
"""Test getting snap lines with guides"""
|
|
snap = SnappingSystem()
|
|
snap.snap_to_edges = False
|
|
snap.snap_to_grid = False
|
|
snap.snap_to_guides = True
|
|
|
|
snap.add_guide(50.0, "vertical")
|
|
snap.add_guide(100.0, "horizontal")
|
|
|
|
result = snap.get_snap_lines((210, 297))
|
|
|
|
assert len(result["guides"]) == 2
|
|
|
|
|
|
class TestSerialization:
|
|
"""Tests for serialize/deserialize methods"""
|
|
|
|
def test_serialize(self):
|
|
"""Test serialization"""
|
|
snap = SnappingSystem(snap_threshold_mm=8.0)
|
|
snap.grid_size_mm = 15.0
|
|
snap.snap_to_grid = True
|
|
snap.snap_to_edges = False
|
|
snap.add_guide(50.0, "vertical")
|
|
snap.add_guide(100.0, "horizontal")
|
|
|
|
data = snap.serialize()
|
|
|
|
assert data["snap_threshold_mm"] == 8.0
|
|
assert data["grid_size_mm"] == 15.0
|
|
assert data["snap_to_grid"] is True
|
|
assert data["snap_to_edges"] is False
|
|
assert data["snap_to_guides"] is True
|
|
assert len(data["guides"]) == 2
|
|
|
|
def test_deserialize(self):
|
|
"""Test deserialization"""
|
|
snap = SnappingSystem()
|
|
|
|
data = {
|
|
"snap_threshold_mm": 12.0,
|
|
"grid_size_mm": 20.0,
|
|
"snap_to_grid": True,
|
|
"snap_to_edges": False,
|
|
"snap_to_guides": False,
|
|
"guides": [{"position": 75.0, "orientation": "vertical"}, {"position": 125.0, "orientation": "horizontal"}],
|
|
}
|
|
|
|
snap.deserialize(data)
|
|
|
|
assert snap.snap_threshold_mm == 12.0
|
|
assert snap.grid_size_mm == 20.0
|
|
assert snap.snap_to_grid is True
|
|
assert snap.snap_to_edges is False
|
|
assert snap.snap_to_guides is False
|
|
assert len(snap.guides) == 2
|
|
assert snap.guides[0].position == 75.0
|
|
assert snap.guides[1].orientation == "horizontal"
|
|
|
|
def test_serialize_deserialize_roundtrip(self):
|
|
"""Test serialize/deserialize roundtrip"""
|
|
original = SnappingSystem(snap_threshold_mm=7.5)
|
|
original.grid_size_mm = 12.5
|
|
original.snap_to_grid = True
|
|
original.add_guide(33.0, "vertical")
|
|
original.add_guide(66.0, "horizontal")
|
|
|
|
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)
|
|
|
|
def test_deserialize_defaults(self):
|
|
"""Test deserialization with missing fields uses defaults"""
|
|
snap = SnappingSystem()
|
|
snap.deserialize({})
|
|
|
|
assert snap.snap_threshold_mm == 5.0
|
|
assert snap.grid_size_mm == 10.0
|
|
assert snap.snap_to_grid is False
|
|
assert snap.snap_to_edges is True
|
|
assert snap.snap_to_guides is True
|
|
assert snap.guides == []
|