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
512 lines
18 KiB
Python
Executable File
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
|