Added styling
Python CI / test (push) Successful in 1m44s
Lint / lint (push) Successful in 1m29s
Tests / test (3.11) (push) Successful in 1m49s
Tests / test (3.12) (push) Successful in 1m52s
Tests / test (3.13) (push) Successful in 1m45s
Tests / test (3.14) (push) Successful in 1m28s
Python CI / test (push) Successful in 1m44s
Lint / lint (push) Successful in 1m29s
Tests / test (3.11) (push) Successful in 1m49s
Tests / test (3.12) (push) Successful in 1m52s
Tests / test (3.13) (push) Successful in 1m45s
Tests / test (3.14) (push) Successful in 1m28s
Improved pdf generation speed
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Unit tests for FrameManager class and frame definitions
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pyPhotoAlbum.frame_manager import (
|
||||
FrameManager,
|
||||
FrameDefinition,
|
||||
FrameCategory,
|
||||
FrameType,
|
||||
get_frame_manager,
|
||||
)
|
||||
|
||||
|
||||
class TestFrameCategory:
|
||||
"""Tests for FrameCategory enum"""
|
||||
|
||||
def test_modern_category_value(self):
|
||||
"""Test MODERN category has correct value"""
|
||||
assert FrameCategory.MODERN.value == "modern"
|
||||
|
||||
def test_vintage_category_value(self):
|
||||
"""Test VINTAGE category has correct value"""
|
||||
assert FrameCategory.VINTAGE.value == "vintage"
|
||||
|
||||
def test_geometric_category_value(self):
|
||||
"""Test GEOMETRIC category has correct value"""
|
||||
assert FrameCategory.GEOMETRIC.value == "geometric"
|
||||
|
||||
def test_custom_category_value(self):
|
||||
"""Test CUSTOM category has correct value"""
|
||||
assert FrameCategory.CUSTOM.value == "custom"
|
||||
|
||||
|
||||
class TestFrameType:
|
||||
"""Tests for FrameType enum"""
|
||||
|
||||
def test_corners_type_value(self):
|
||||
"""Test CORNERS type has correct value"""
|
||||
assert FrameType.CORNERS.value == "corners"
|
||||
|
||||
def test_full_type_value(self):
|
||||
"""Test FULL type has correct value"""
|
||||
assert FrameType.FULL.value == "full"
|
||||
|
||||
def test_edges_type_value(self):
|
||||
"""Test EDGES type has correct value"""
|
||||
assert FrameType.EDGES.value == "edges"
|
||||
|
||||
|
||||
class TestFrameDefinition:
|
||||
"""Tests for FrameDefinition dataclass"""
|
||||
|
||||
def test_basic_frame_definition(self):
|
||||
"""Test creating a basic frame definition"""
|
||||
frame = FrameDefinition(
|
||||
name="test_frame",
|
||||
display_name="Test Frame",
|
||||
category=FrameCategory.MODERN,
|
||||
frame_type=FrameType.FULL,
|
||||
description="A test frame",
|
||||
)
|
||||
assert frame.name == "test_frame"
|
||||
assert frame.display_name == "Test Frame"
|
||||
assert frame.category == FrameCategory.MODERN
|
||||
assert frame.frame_type == FrameType.FULL
|
||||
assert frame.description == "A test frame"
|
||||
|
||||
def test_frame_definition_defaults(self):
|
||||
"""Test frame definition default values"""
|
||||
frame = FrameDefinition(
|
||||
name="minimal",
|
||||
display_name="Minimal",
|
||||
category=FrameCategory.MODERN,
|
||||
frame_type=FrameType.FULL,
|
||||
)
|
||||
assert frame.description == ""
|
||||
assert frame.assets == {}
|
||||
assert frame.colorizable is True
|
||||
assert frame.default_thickness == 5.0
|
||||
|
||||
def test_corner_type_frame(self):
|
||||
"""Test creating a corner-type frame"""
|
||||
frame = FrameDefinition(
|
||||
name="corners",
|
||||
display_name="Corners",
|
||||
category=FrameCategory.VINTAGE,
|
||||
frame_type=FrameType.CORNERS,
|
||||
default_thickness=8.0,
|
||||
)
|
||||
assert frame.frame_type == FrameType.CORNERS
|
||||
assert frame.default_thickness == 8.0
|
||||
|
||||
|
||||
class TestFrameManager:
|
||||
"""Tests for FrameManager class"""
|
||||
|
||||
@pytest.fixture
|
||||
def frame_manager(self):
|
||||
"""Create a fresh FrameManager instance"""
|
||||
return FrameManager()
|
||||
|
||||
def test_frame_manager_has_frames(self, frame_manager):
|
||||
"""Test that FrameManager loads bundled frames"""
|
||||
frames = frame_manager.get_all_frames()
|
||||
assert len(frames) > 0
|
||||
|
||||
def test_get_frame_by_name(self, frame_manager):
|
||||
"""Test getting a frame by name"""
|
||||
frame = frame_manager.get_frame("simple_line")
|
||||
assert frame is not None
|
||||
assert frame.name == "simple_line"
|
||||
assert frame.display_name == "Simple Line"
|
||||
|
||||
def test_get_nonexistent_frame(self, frame_manager):
|
||||
"""Test getting a nonexistent frame returns None"""
|
||||
frame = frame_manager.get_frame("nonexistent_frame")
|
||||
assert frame is None
|
||||
|
||||
def test_get_frames_by_category_modern(self, frame_manager):
|
||||
"""Test getting frames by MODERN category"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.MODERN)
|
||||
assert len(frames) > 0
|
||||
for frame in frames:
|
||||
assert frame.category == FrameCategory.MODERN
|
||||
|
||||
def test_get_frames_by_category_vintage(self, frame_manager):
|
||||
"""Test getting frames by VINTAGE category"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.VINTAGE)
|
||||
assert len(frames) > 0
|
||||
for frame in frames:
|
||||
assert frame.category == FrameCategory.VINTAGE
|
||||
|
||||
def test_get_frames_by_category_geometric(self, frame_manager):
|
||||
"""Test getting frames by GEOMETRIC category"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.GEOMETRIC)
|
||||
assert len(frames) > 0
|
||||
for frame in frames:
|
||||
assert frame.category == FrameCategory.GEOMETRIC
|
||||
|
||||
def test_get_frame_names(self, frame_manager):
|
||||
"""Test getting list of frame names"""
|
||||
names = frame_manager.get_frame_names()
|
||||
assert isinstance(names, list)
|
||||
assert "simple_line" in names
|
||||
assert "double_line" in names
|
||||
assert "leafy_corners" in names
|
||||
|
||||
def test_bundled_frames_exist(self, frame_manager):
|
||||
"""Test that expected bundled frames exist"""
|
||||
expected_frames = [
|
||||
"simple_line",
|
||||
"double_line",
|
||||
"rounded_modern",
|
||||
"geometric_corners",
|
||||
"leafy_corners",
|
||||
"ornate_flourish",
|
||||
"victorian",
|
||||
"art_nouveau",
|
||||
]
|
||||
for name in expected_frames:
|
||||
frame = frame_manager.get_frame(name)
|
||||
assert frame is not None, f"Expected frame '{name}' not found"
|
||||
|
||||
def test_modern_frames_are_full_type(self, frame_manager):
|
||||
"""Test that modern frames are FULL type"""
|
||||
modern_frames = ["simple_line", "double_line", "rounded_modern"]
|
||||
for name in modern_frames:
|
||||
frame = frame_manager.get_frame(name)
|
||||
assert frame is not None
|
||||
assert frame.frame_type == FrameType.FULL
|
||||
|
||||
def test_leafy_corners_is_corners_type(self, frame_manager):
|
||||
"""Test that leafy_corners is CORNERS type"""
|
||||
frame = frame_manager.get_frame("leafy_corners")
|
||||
assert frame is not None
|
||||
assert frame.frame_type == FrameType.CORNERS
|
||||
|
||||
def test_all_frames_are_colorizable(self, frame_manager):
|
||||
"""Test that all bundled frames are colorizable"""
|
||||
for frame in frame_manager.get_all_frames():
|
||||
assert frame.colorizable is True
|
||||
|
||||
|
||||
class TestGetFrameManager:
|
||||
"""Tests for global frame manager instance"""
|
||||
|
||||
def test_get_frame_manager_returns_instance(self):
|
||||
"""Test that get_frame_manager returns a FrameManager"""
|
||||
manager = get_frame_manager()
|
||||
assert isinstance(manager, FrameManager)
|
||||
|
||||
def test_get_frame_manager_returns_same_instance(self):
|
||||
"""Test that get_frame_manager returns the same instance"""
|
||||
manager1 = get_frame_manager()
|
||||
manager2 = get_frame_manager()
|
||||
assert manager1 is manager2
|
||||
|
||||
|
||||
class TestFrameCategories:
|
||||
"""Tests for frame category organization"""
|
||||
|
||||
@pytest.fixture
|
||||
def frame_manager(self):
|
||||
return FrameManager()
|
||||
|
||||
def test_modern_category_not_empty(self, frame_manager):
|
||||
"""Test MODERN category has frames"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.MODERN)
|
||||
assert len(frames) >= 3 # simple_line, double_line, rounded_modern
|
||||
|
||||
def test_vintage_category_not_empty(self, frame_manager):
|
||||
"""Test VINTAGE category has frames"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.VINTAGE)
|
||||
assert len(frames) >= 3 # leafy_corners, ornate_flourish, victorian, art_nouveau
|
||||
|
||||
def test_geometric_category_not_empty(self, frame_manager):
|
||||
"""Test GEOMETRIC category has frames"""
|
||||
frames = frame_manager.get_frames_by_category(FrameCategory.GEOMETRIC)
|
||||
assert len(frames) >= 1 # geometric_corners
|
||||
|
||||
def test_all_frames_count(self, frame_manager):
|
||||
"""Test total frame count"""
|
||||
all_frames = frame_manager.get_all_frames()
|
||||
# Should be at least 8 bundled frames
|
||||
assert len(all_frames) >= 8
|
||||
|
||||
|
||||
class TestFrameDescriptions:
|
||||
"""Tests for frame descriptions"""
|
||||
|
||||
@pytest.fixture
|
||||
def frame_manager(self):
|
||||
return FrameManager()
|
||||
|
||||
def test_simple_line_has_description(self, frame_manager):
|
||||
"""Test simple_line has a description"""
|
||||
frame = frame_manager.get_frame("simple_line")
|
||||
assert frame.description != ""
|
||||
|
||||
def test_leafy_corners_has_description(self, frame_manager):
|
||||
"""Test leafy_corners has a description"""
|
||||
frame = frame_manager.get_frame("leafy_corners")
|
||||
assert frame.description != ""
|
||||
|
||||
def test_all_frames_have_descriptions(self, frame_manager):
|
||||
"""Test all frames have non-empty descriptions"""
|
||||
for frame in frame_manager.get_all_frames():
|
||||
assert frame.description != "", f"Frame '{frame.name}' has no description"
|
||||
|
||||
def test_all_frames_have_display_names(self, frame_manager):
|
||||
"""Test all frames have display names"""
|
||||
for frame in frame_manager.get_all_frames():
|
||||
assert frame.display_name != "", f"Frame '{frame.name}' has no display name"
|
||||
|
||||
|
||||
class TestFrameThickness:
|
||||
"""Tests for frame default thickness values"""
|
||||
|
||||
@pytest.fixture
|
||||
def frame_manager(self):
|
||||
return FrameManager()
|
||||
|
||||
def test_simple_line_thickness(self, frame_manager):
|
||||
"""Test simple_line has thin default thickness"""
|
||||
frame = frame_manager.get_frame("simple_line")
|
||||
assert frame.default_thickness == 2.0
|
||||
|
||||
def test_vintage_frames_are_thicker(self, frame_manager):
|
||||
"""Test vintage frames have thicker default"""
|
||||
leafy = frame_manager.get_frame("leafy_corners")
|
||||
victorian = frame_manager.get_frame("victorian")
|
||||
|
||||
assert leafy.default_thickness >= 8.0
|
||||
assert victorian.default_thickness >= 10.0
|
||||
|
||||
def test_all_thicknesses_positive(self, frame_manager):
|
||||
"""Test all frames have positive thickness"""
|
||||
for frame in frame_manager.get_all_frames():
|
||||
assert frame.default_thickness > 0
|
||||
@@ -0,0 +1,407 @@
|
||||
"""
|
||||
Unit tests for ImageStyle class and related styling functionality
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pyPhotoAlbum.models import ImageStyle, ImageData, PlaceholderData
|
||||
|
||||
|
||||
class TestImageStyleInit:
|
||||
"""Tests for ImageStyle initialization"""
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test ImageStyle with default values"""
|
||||
style = ImageStyle()
|
||||
assert style.corner_radius == 0.0
|
||||
assert style.border_width == 0.0
|
||||
assert style.border_color == (0, 0, 0)
|
||||
assert style.shadow_enabled is False
|
||||
assert style.shadow_offset == (2.0, 2.0)
|
||||
assert style.shadow_blur == 3.0
|
||||
assert style.shadow_color == (0, 0, 0, 128)
|
||||
assert style.frame_style is None
|
||||
assert style.frame_color == (0, 0, 0)
|
||||
assert style.frame_corners == (True, True, True, True)
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test ImageStyle with custom values"""
|
||||
style = ImageStyle(
|
||||
corner_radius=15.0,
|
||||
border_width=2.5,
|
||||
border_color=(255, 0, 0),
|
||||
shadow_enabled=True,
|
||||
shadow_offset=(3.0, 4.0),
|
||||
shadow_blur=5.0,
|
||||
shadow_color=(0, 0, 0, 200),
|
||||
frame_style="leafy_corners",
|
||||
frame_color=(0, 128, 0),
|
||||
frame_corners=(True, False, True, False),
|
||||
)
|
||||
assert style.corner_radius == 15.0
|
||||
assert style.border_width == 2.5
|
||||
assert style.border_color == (255, 0, 0)
|
||||
assert style.shadow_enabled is True
|
||||
assert style.shadow_offset == (3.0, 4.0)
|
||||
assert style.shadow_blur == 5.0
|
||||
assert style.shadow_color == (0, 0, 0, 200)
|
||||
assert style.frame_style == "leafy_corners"
|
||||
assert style.frame_color == (0, 128, 0)
|
||||
assert style.frame_corners == (True, False, True, False)
|
||||
|
||||
def test_frame_corners_none_defaults_to_all(self):
|
||||
"""Test that frame_corners=None defaults to all corners"""
|
||||
style = ImageStyle(frame_corners=None)
|
||||
assert style.frame_corners == (True, True, True, True)
|
||||
|
||||
|
||||
class TestImageStyleCopy:
|
||||
"""Tests for ImageStyle.copy()"""
|
||||
|
||||
def test_copy_creates_identical_style(self):
|
||||
"""Test that copy creates an identical style"""
|
||||
original = ImageStyle(
|
||||
corner_radius=10.0,
|
||||
border_width=1.5,
|
||||
border_color=(128, 128, 128),
|
||||
shadow_enabled=True,
|
||||
frame_style="simple_line",
|
||||
frame_corners=(True, True, False, False),
|
||||
)
|
||||
copied = original.copy()
|
||||
|
||||
assert copied.corner_radius == original.corner_radius
|
||||
assert copied.border_width == original.border_width
|
||||
assert copied.border_color == original.border_color
|
||||
assert copied.shadow_enabled == original.shadow_enabled
|
||||
assert copied.frame_style == original.frame_style
|
||||
assert copied.frame_corners == original.frame_corners
|
||||
|
||||
def test_copy_is_independent(self):
|
||||
"""Test that modifying copy doesn't affect original"""
|
||||
original = ImageStyle(corner_radius=5.0)
|
||||
copied = original.copy()
|
||||
copied.corner_radius = 20.0
|
||||
|
||||
assert original.corner_radius == 5.0
|
||||
assert copied.corner_radius == 20.0
|
||||
|
||||
|
||||
class TestImageStyleHasStyling:
|
||||
"""Tests for ImageStyle.has_styling()"""
|
||||
|
||||
def test_default_style_has_no_styling(self):
|
||||
"""Test that default style has no styling"""
|
||||
style = ImageStyle()
|
||||
assert style.has_styling() is False
|
||||
|
||||
def test_corner_radius_counts_as_styling(self):
|
||||
"""Test that corner_radius > 0 counts as styling"""
|
||||
style = ImageStyle(corner_radius=5.0)
|
||||
assert style.has_styling() is True
|
||||
|
||||
def test_border_width_counts_as_styling(self):
|
||||
"""Test that border_width > 0 counts as styling"""
|
||||
style = ImageStyle(border_width=1.0)
|
||||
assert style.has_styling() is True
|
||||
|
||||
def test_shadow_enabled_counts_as_styling(self):
|
||||
"""Test that shadow_enabled counts as styling"""
|
||||
style = ImageStyle(shadow_enabled=True)
|
||||
assert style.has_styling() is True
|
||||
|
||||
def test_frame_style_counts_as_styling(self):
|
||||
"""Test that frame_style counts as styling"""
|
||||
style = ImageStyle(frame_style="simple_line")
|
||||
assert style.has_styling() is True
|
||||
|
||||
|
||||
class TestImageStyleSerialization:
|
||||
"""Tests for ImageStyle.serialize() and deserialize()"""
|
||||
|
||||
def test_serialize_default_style(self):
|
||||
"""Test serialization of default style"""
|
||||
style = ImageStyle()
|
||||
data = style.serialize()
|
||||
|
||||
assert data["corner_radius"] == 0.0
|
||||
assert data["border_width"] == 0.0
|
||||
assert data["border_color"] == [0, 0, 0]
|
||||
assert data["shadow_enabled"] is False
|
||||
assert data["frame_style"] is None
|
||||
assert data["frame_corners"] == [True, True, True, True]
|
||||
|
||||
def test_serialize_custom_style(self):
|
||||
"""Test serialization of custom style"""
|
||||
style = ImageStyle(
|
||||
corner_radius=10.0,
|
||||
border_color=(255, 128, 0),
|
||||
frame_style="ornate_flourish",
|
||||
frame_corners=(False, True, False, True),
|
||||
)
|
||||
data = style.serialize()
|
||||
|
||||
assert data["corner_radius"] == 10.0
|
||||
assert data["border_color"] == [255, 128, 0]
|
||||
assert data["frame_style"] == "ornate_flourish"
|
||||
assert data["frame_corners"] == [False, True, False, True]
|
||||
|
||||
def test_deserialize_creates_correct_style(self):
|
||||
"""Test deserialization creates correct style"""
|
||||
data = {
|
||||
"corner_radius": 15.0,
|
||||
"border_width": 2.0,
|
||||
"border_color": [100, 100, 100],
|
||||
"shadow_enabled": True,
|
||||
"shadow_offset": [3.0, 3.0],
|
||||
"shadow_blur": 4.0,
|
||||
"shadow_color": [0, 0, 0, 150],
|
||||
"frame_style": "leafy_corners",
|
||||
"frame_color": [50, 50, 50],
|
||||
"frame_corners": [True, False, True, False],
|
||||
}
|
||||
style = ImageStyle.deserialize(data)
|
||||
|
||||
assert style.corner_radius == 15.0
|
||||
assert style.border_width == 2.0
|
||||
assert style.border_color == (100, 100, 100)
|
||||
assert style.shadow_enabled is True
|
||||
assert style.shadow_offset == (3.0, 3.0)
|
||||
assert style.shadow_blur == 4.0
|
||||
assert style.shadow_color == (0, 0, 0, 150)
|
||||
assert style.frame_style == "leafy_corners"
|
||||
assert style.frame_color == (50, 50, 50)
|
||||
assert style.frame_corners == (True, False, True, False)
|
||||
|
||||
def test_deserialize_none_returns_default(self):
|
||||
"""Test that deserialize(None) returns default style"""
|
||||
style = ImageStyle.deserialize(None)
|
||||
assert style.corner_radius == 0.0
|
||||
assert style.frame_style is None
|
||||
|
||||
def test_deserialize_empty_dict_returns_defaults(self):
|
||||
"""Test that deserialize({}) returns default values"""
|
||||
style = ImageStyle.deserialize({})
|
||||
assert style.corner_radius == 0.0
|
||||
assert style.border_width == 0.0
|
||||
assert style.frame_style is None
|
||||
assert style.frame_corners == (True, True, True, True)
|
||||
|
||||
def test_serialize_deserialize_roundtrip(self):
|
||||
"""Test that serialize/deserialize are inverse operations"""
|
||||
original = ImageStyle(
|
||||
corner_radius=12.0,
|
||||
border_width=1.5,
|
||||
border_color=(200, 100, 50),
|
||||
shadow_enabled=True,
|
||||
shadow_offset=(2.5, 3.5),
|
||||
shadow_blur=6.0,
|
||||
shadow_color=(10, 20, 30, 180),
|
||||
frame_style="victorian",
|
||||
frame_color=(100, 150, 200),
|
||||
frame_corners=(True, True, False, False),
|
||||
)
|
||||
|
||||
data = original.serialize()
|
||||
restored = ImageStyle.deserialize(data)
|
||||
|
||||
assert restored == original
|
||||
|
||||
def test_deserialize_handles_missing_frame_corners(self):
|
||||
"""Test backward compatibility - missing frame_corners defaults correctly"""
|
||||
data = {
|
||||
"corner_radius": 5.0,
|
||||
"border_width": 1.0,
|
||||
"border_color": [0, 0, 0],
|
||||
# No frame_corners key
|
||||
}
|
||||
style = ImageStyle.deserialize(data)
|
||||
assert style.frame_corners == (True, True, True, True)
|
||||
|
||||
|
||||
class TestImageStyleEquality:
|
||||
"""Tests for ImageStyle.__eq__()"""
|
||||
|
||||
def test_identical_styles_are_equal(self):
|
||||
"""Test that identical styles are equal"""
|
||||
style1 = ImageStyle(corner_radius=10.0, frame_style="simple_line")
|
||||
style2 = ImageStyle(corner_radius=10.0, frame_style="simple_line")
|
||||
assert style1 == style2
|
||||
|
||||
def test_different_styles_are_not_equal(self):
|
||||
"""Test that different styles are not equal"""
|
||||
style1 = ImageStyle(corner_radius=10.0)
|
||||
style2 = ImageStyle(corner_radius=20.0)
|
||||
assert style1 != style2
|
||||
|
||||
def test_different_frame_corners_are_not_equal(self):
|
||||
"""Test that different frame_corners make styles unequal"""
|
||||
style1 = ImageStyle(frame_corners=(True, True, True, True))
|
||||
style2 = ImageStyle(frame_corners=(True, False, True, False))
|
||||
assert style1 != style2
|
||||
|
||||
def test_style_not_equal_to_non_style(self):
|
||||
"""Test that style is not equal to non-style objects"""
|
||||
style = ImageStyle()
|
||||
assert style != "not a style"
|
||||
assert style != 123
|
||||
assert style != None
|
||||
|
||||
|
||||
class TestImageDataWithStyle:
|
||||
"""Tests for ImageData with styling"""
|
||||
|
||||
def test_image_data_has_default_style(self):
|
||||
"""Test that ImageData has a default style"""
|
||||
img = ImageData()
|
||||
assert hasattr(img, "style")
|
||||
assert isinstance(img.style, ImageStyle)
|
||||
assert img.style.corner_radius == 0.0
|
||||
|
||||
def test_image_data_serialize_includes_style(self):
|
||||
"""Test that ImageData serialization includes style"""
|
||||
img = ImageData()
|
||||
img.style.corner_radius = 15.0
|
||||
img.style.frame_style = "ornate_flourish"
|
||||
img.style.frame_corners = (True, False, False, True)
|
||||
|
||||
data = img.serialize()
|
||||
|
||||
assert "style" in data
|
||||
assert data["style"]["corner_radius"] == 15.0
|
||||
assert data["style"]["frame_style"] == "ornate_flourish"
|
||||
assert data["style"]["frame_corners"] == [True, False, False, True]
|
||||
|
||||
def test_image_data_deserialize_restores_style(self):
|
||||
"""Test that ImageData deserialization restores style"""
|
||||
img = ImageData()
|
||||
data = {
|
||||
"style": {
|
||||
"corner_radius": 20.0,
|
||||
"border_width": 2.0,
|
||||
"frame_style": "leafy_corners",
|
||||
"frame_corners": [False, True, False, True],
|
||||
}
|
||||
}
|
||||
img.deserialize(data)
|
||||
|
||||
assert img.style.corner_radius == 20.0
|
||||
assert img.style.border_width == 2.0
|
||||
assert img.style.frame_style == "leafy_corners"
|
||||
assert img.style.frame_corners == (False, True, False, True)
|
||||
|
||||
def test_image_data_deserialize_without_style(self):
|
||||
"""Test backward compatibility - deserialize without style"""
|
||||
img = ImageData()
|
||||
data = {"image_path": "test.jpg"}
|
||||
img.deserialize(data)
|
||||
|
||||
# Should have default style
|
||||
assert img.style.corner_radius == 0.0
|
||||
assert img.style.frame_style is None
|
||||
|
||||
|
||||
class TestPlaceholderDataWithStyle:
|
||||
"""Tests for PlaceholderData with styling"""
|
||||
|
||||
def test_placeholder_has_default_style(self):
|
||||
"""Test that PlaceholderData has a default style"""
|
||||
placeholder = PlaceholderData()
|
||||
assert hasattr(placeholder, "style")
|
||||
assert isinstance(placeholder.style, ImageStyle)
|
||||
|
||||
def test_placeholder_serialize_includes_style(self):
|
||||
"""Test that PlaceholderData serialization includes style"""
|
||||
placeholder = PlaceholderData()
|
||||
placeholder.style.corner_radius = 10.0
|
||||
placeholder.style.shadow_enabled = True
|
||||
placeholder.style.frame_corners = (True, True, False, False)
|
||||
|
||||
data = placeholder.serialize()
|
||||
|
||||
assert "style" in data
|
||||
assert data["style"]["corner_radius"] == 10.0
|
||||
assert data["style"]["shadow_enabled"] is True
|
||||
|
||||
def test_placeholder_deserialize_restores_style(self):
|
||||
"""Test that PlaceholderData deserialization restores style"""
|
||||
placeholder = PlaceholderData()
|
||||
data = {
|
||||
"style": {
|
||||
"corner_radius": 5.0,
|
||||
"border_width": 1.5,
|
||||
"frame_style": "double_line",
|
||||
}
|
||||
}
|
||||
placeholder.deserialize(data)
|
||||
|
||||
assert placeholder.style.corner_radius == 5.0
|
||||
assert placeholder.style.border_width == 1.5
|
||||
assert placeholder.style.frame_style == "double_line"
|
||||
|
||||
|
||||
class TestFrameCorners:
|
||||
"""Tests specifically for frame_corners functionality"""
|
||||
|
||||
def test_all_corners_true(self):
|
||||
"""Test all corners enabled"""
|
||||
style = ImageStyle(frame_corners=(True, True, True, True))
|
||||
assert all(style.frame_corners)
|
||||
|
||||
def test_no_corners(self):
|
||||
"""Test no corners enabled"""
|
||||
style = ImageStyle(frame_corners=(False, False, False, False))
|
||||
assert not any(style.frame_corners)
|
||||
|
||||
def test_diagonal_corners_only(self):
|
||||
"""Test diagonal corners (TL, BR)"""
|
||||
style = ImageStyle(frame_corners=(True, False, True, False))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is True
|
||||
assert tr is False
|
||||
assert br is True
|
||||
assert bl is False
|
||||
|
||||
def test_opposite_diagonal_corners(self):
|
||||
"""Test opposite diagonal corners (TR, BL)"""
|
||||
style = ImageStyle(frame_corners=(False, True, False, True))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is False
|
||||
assert tr is True
|
||||
assert br is False
|
||||
assert bl is True
|
||||
|
||||
def test_left_corners_only(self):
|
||||
"""Test left side corners only"""
|
||||
style = ImageStyle(frame_corners=(True, False, False, True))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is True
|
||||
assert tr is False
|
||||
assert br is False
|
||||
assert bl is True
|
||||
|
||||
def test_right_corners_only(self):
|
||||
"""Test right side corners only"""
|
||||
style = ImageStyle(frame_corners=(False, True, True, False))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is False
|
||||
assert tr is True
|
||||
assert br is True
|
||||
assert bl is False
|
||||
|
||||
def test_top_corners_only(self):
|
||||
"""Test top corners only"""
|
||||
style = ImageStyle(frame_corners=(True, True, False, False))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is True
|
||||
assert tr is True
|
||||
assert br is False
|
||||
assert bl is False
|
||||
|
||||
def test_bottom_corners_only(self):
|
||||
"""Test bottom corners only"""
|
||||
style = ImageStyle(frame_corners=(False, False, True, True))
|
||||
tl, tr, br, bl = style.frame_corners
|
||||
assert tl is False
|
||||
assert tr is False
|
||||
assert br is True
|
||||
assert bl is True
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
Unit tests for image_utils styling functions
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from pyPhotoAlbum.image_utils import (
|
||||
apply_rounded_corners,
|
||||
apply_drop_shadow,
|
||||
create_border_image,
|
||||
)
|
||||
|
||||
|
||||
class TestApplyRoundedCorners:
|
||||
"""Tests for apply_rounded_corners function"""
|
||||
|
||||
def test_zero_radius_returns_same_image(self):
|
||||
"""Test that 0% radius returns unchanged image"""
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
result = apply_rounded_corners(img, 0.0)
|
||||
assert result.size == img.size
|
||||
|
||||
def test_negative_radius_returns_same_image(self):
|
||||
"""Test that negative radius returns unchanged image"""
|
||||
img = Image.new("RGB", (100, 100), color="blue")
|
||||
result = apply_rounded_corners(img, -10.0)
|
||||
assert result.size == img.size
|
||||
|
||||
def test_returns_rgba_image(self):
|
||||
"""Test that result is RGBA mode"""
|
||||
img = Image.new("RGB", (100, 100), color="green")
|
||||
result = apply_rounded_corners(img, 10.0)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_preserves_size(self):
|
||||
"""Test that image size is preserved"""
|
||||
img = Image.new("RGBA", (200, 150), color="yellow")
|
||||
result = apply_rounded_corners(img, 15.0)
|
||||
assert result.size == (200, 150)
|
||||
|
||||
def test_corners_are_transparent(self):
|
||||
"""Test that corners become transparent"""
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
result = apply_rounded_corners(img, 25.0)
|
||||
|
||||
# Top-left corner should be transparent
|
||||
pixel = result.getpixel((0, 0))
|
||||
assert pixel[3] == 0, "Top-left corner should be transparent"
|
||||
|
||||
# Top-right corner should be transparent
|
||||
pixel = result.getpixel((99, 0))
|
||||
assert pixel[3] == 0, "Top-right corner should be transparent"
|
||||
|
||||
# Bottom-left corner should be transparent
|
||||
pixel = result.getpixel((0, 99))
|
||||
assert pixel[3] == 0, "Bottom-left corner should be transparent"
|
||||
|
||||
# Bottom-right corner should be transparent
|
||||
pixel = result.getpixel((99, 99))
|
||||
assert pixel[3] == 0, "Bottom-right corner should be transparent"
|
||||
|
||||
def test_center_is_opaque(self):
|
||||
"""Test that center remains opaque"""
|
||||
img = Image.new("RGB", (100, 100), color="blue")
|
||||
result = apply_rounded_corners(img, 10.0)
|
||||
|
||||
# Center pixel should be fully opaque
|
||||
pixel = result.getpixel((50, 50))
|
||||
assert pixel[3] == 255, "Center should be opaque"
|
||||
|
||||
def test_50_percent_radius_creates_ellipse(self):
|
||||
"""Test that 50% radius creates elliptical result"""
|
||||
img = Image.new("RGB", (100, 100), color="purple")
|
||||
result = apply_rounded_corners(img, 50.0)
|
||||
|
||||
# Corner should be transparent
|
||||
assert result.getpixel((0, 0))[3] == 0
|
||||
|
||||
def test_radius_clamped_to_50(self):
|
||||
"""Test that radius > 50 is clamped"""
|
||||
img = Image.new("RGB", (100, 100), color="orange")
|
||||
result = apply_rounded_corners(img, 100.0) # Should be clamped to 50
|
||||
|
||||
# Should still work and not crash
|
||||
assert result.size == (100, 100)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_preserves_existing_rgba(self):
|
||||
"""Test that existing RGBA image alpha is preserved"""
|
||||
img = Image.new("RGBA", (100, 100), (255, 0, 0, 128)) # Semi-transparent red
|
||||
result = apply_rounded_corners(img, 10.0)
|
||||
|
||||
# Center should still be semi-transparent (original alpha combined with mask)
|
||||
center_pixel = result.getpixel((50, 50))
|
||||
assert center_pixel[3] == 128 # Original alpha preserved
|
||||
|
||||
|
||||
class TestApplyDropShadow:
|
||||
"""Tests for apply_drop_shadow function"""
|
||||
|
||||
def test_returns_rgba_image(self):
|
||||
"""Test that result is RGBA mode"""
|
||||
img = Image.new("RGB", (50, 50), color="red")
|
||||
result = apply_drop_shadow(img)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_expand_increases_size(self):
|
||||
"""Test that expand=True increases canvas size"""
|
||||
img = Image.new("RGBA", (50, 50), color="blue")
|
||||
result = apply_drop_shadow(img, offset=(5, 5), blur_radius=3, expand=True)
|
||||
assert result.width > 50
|
||||
assert result.height > 50
|
||||
|
||||
def test_no_expand_preserves_size(self):
|
||||
"""Test that expand=False preserves size"""
|
||||
img = Image.new("RGBA", (50, 50), color="green")
|
||||
result = apply_drop_shadow(img, offset=(2, 2), blur_radius=3, expand=False)
|
||||
assert result.size == (50, 50)
|
||||
|
||||
def test_shadow_has_correct_color(self):
|
||||
"""Test that shadow uses specified color"""
|
||||
# Create image with transparent background and opaque center
|
||||
img = Image.new("RGBA", (20, 20), (0, 0, 0, 0))
|
||||
img.paste((255, 0, 0, 255), (5, 5, 15, 15))
|
||||
|
||||
result = apply_drop_shadow(
|
||||
img, offset=(10, 10), blur_radius=0, shadow_color=(0, 255, 0, 255), expand=True
|
||||
)
|
||||
|
||||
# Shadow should be visible in the offset area
|
||||
# The shadow color should be green
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_zero_blur_radius(self):
|
||||
"""Test that blur_radius=0 works"""
|
||||
img = Image.new("RGBA", (30, 30), (255, 0, 0, 255))
|
||||
result = apply_drop_shadow(img, blur_radius=0, expand=True)
|
||||
assert result.mode == "RGBA"
|
||||
assert result.width >= 30
|
||||
assert result.height >= 30
|
||||
|
||||
def test_large_offset(self):
|
||||
"""Test shadow with large offset"""
|
||||
img = Image.new("RGBA", (50, 50), (0, 0, 255, 255))
|
||||
result = apply_drop_shadow(img, offset=(20, 20), blur_radius=5, expand=True)
|
||||
assert result.width > 50 + 20
|
||||
assert result.height > 50 + 20
|
||||
|
||||
def test_negative_offset(self):
|
||||
"""Test shadow with negative offset (shadow above/left of image)"""
|
||||
img = Image.new("RGBA", (50, 50), (255, 255, 0, 255))
|
||||
result = apply_drop_shadow(img, offset=(-10, -10), blur_radius=2, expand=True)
|
||||
assert result.width > 50
|
||||
assert result.height > 50
|
||||
|
||||
|
||||
class TestCreateBorderImage:
|
||||
"""Tests for create_border_image function"""
|
||||
|
||||
def test_returns_rgba_image(self):
|
||||
"""Test that result is RGBA mode"""
|
||||
result = create_border_image(100, 100, 5)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_correct_size(self):
|
||||
"""Test that result has correct size"""
|
||||
result = create_border_image(200, 150, 10)
|
||||
assert result.size == (200, 150)
|
||||
|
||||
def test_zero_border_returns_transparent(self):
|
||||
"""Test that 0 border width returns fully transparent image"""
|
||||
result = create_border_image(100, 100, 0)
|
||||
assert result.mode == "RGBA"
|
||||
# All pixels should be transparent
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
assert result.getpixel((x, y))[3] == 0
|
||||
|
||||
def test_border_color_applied(self):
|
||||
"""Test that border color is applied correctly"""
|
||||
result = create_border_image(50, 50, 5, border_color=(255, 0, 0))
|
||||
|
||||
# Edge pixel should be red
|
||||
edge_pixel = result.getpixel((0, 25)) # Left edge, middle
|
||||
assert edge_pixel[:3] == (255, 0, 0)
|
||||
assert edge_pixel[3] == 255 # Opaque
|
||||
|
||||
def test_center_is_transparent(self):
|
||||
"""Test that center is transparent"""
|
||||
result = create_border_image(100, 100, 10)
|
||||
|
||||
# Center pixel should be transparent
|
||||
center_pixel = result.getpixel((50, 50))
|
||||
assert center_pixel[3] == 0
|
||||
|
||||
def test_border_surrounds_image(self):
|
||||
"""Test that border covers all edges"""
|
||||
result = create_border_image(50, 50, 5, border_color=(0, 255, 0))
|
||||
|
||||
# Top edge
|
||||
assert result.getpixel((25, 0))[3] == 255 # Opaque
|
||||
# Bottom edge
|
||||
assert result.getpixel((25, 49))[3] == 255
|
||||
# Left edge
|
||||
assert result.getpixel((0, 25))[3] == 255
|
||||
# Right edge
|
||||
assert result.getpixel((49, 25))[3] == 255
|
||||
|
||||
def test_with_corner_radius(self):
|
||||
"""Test border with rounded corners"""
|
||||
result = create_border_image(100, 100, 10, border_color=(0, 0, 255), corner_radius=20)
|
||||
assert result.mode == "RGBA"
|
||||
assert result.size == (100, 100)
|
||||
|
||||
def test_corner_radius_affects_transparency(self):
|
||||
"""Test that corner radius creates rounded border"""
|
||||
result = create_border_image(100, 100, 10, corner_radius=25)
|
||||
|
||||
# Outer corner should be transparent (outside the rounded border)
|
||||
outer_corner = result.getpixel((0, 0))
|
||||
assert outer_corner[3] == 0
|
||||
|
||||
def test_large_border_width(self):
|
||||
"""Test with large border width"""
|
||||
result = create_border_image(100, 100, 45) # Very thick border
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
# Center should still be transparent (just a small area)
|
||||
center = result.getpixel((50, 50))
|
||||
assert center[3] == 0
|
||||
|
||||
|
||||
class TestStylingIntegration:
|
||||
"""Integration tests combining multiple styling functions"""
|
||||
|
||||
def test_rounded_corners_then_shadow(self):
|
||||
"""Test applying rounded corners then shadow"""
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
rounded = apply_rounded_corners(img, 15.0)
|
||||
result = apply_drop_shadow(rounded, offset=(5, 5), blur_radius=3, expand=True)
|
||||
|
||||
assert result.mode == "RGBA"
|
||||
assert result.width > 100
|
||||
assert result.height > 100
|
||||
|
||||
def test_preserve_quality_through_chain(self):
|
||||
"""Test that chaining operations preserves image quality"""
|
||||
# Create a simple pattern
|
||||
img = Image.new("RGB", (80, 80), color="blue")
|
||||
img.paste((255, 0, 0), (20, 20, 60, 60)) # Red square in center
|
||||
|
||||
# Apply styling chain
|
||||
result = apply_rounded_corners(img, 10.0)
|
||||
result = apply_drop_shadow(result, expand=True)
|
||||
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_small_image_styling(self):
|
||||
"""Test styling on small images"""
|
||||
img = Image.new("RGB", (10, 10), color="green")
|
||||
rounded = apply_rounded_corners(img, 20.0)
|
||||
shadow = apply_drop_shadow(rounded, offset=(1, 1), blur_radius=1, expand=True)
|
||||
|
||||
assert shadow.mode == "RGBA"
|
||||
assert shadow.width >= 10
|
||||
assert shadow.height >= 10
|
||||
|
||||
def test_large_image_styling(self):
|
||||
"""Test styling on larger images"""
|
||||
img = Image.new("RGB", (1000, 800), color="purple")
|
||||
rounded = apply_rounded_corners(img, 5.0)
|
||||
|
||||
assert rounded.mode == "RGBA"
|
||||
assert rounded.size == (1000, 800)
|
||||
|
||||
# Corners should be transparent
|
||||
assert rounded.getpixel((0, 0))[3] == 0
|
||||
assert rounded.getpixel((999, 0))[3] == 0
|
||||
|
||||
def test_non_square_image_styling(self):
|
||||
"""Test styling on non-square images"""
|
||||
img = Image.new("RGB", (200, 50), color="orange")
|
||||
rounded = apply_rounded_corners(img, 30.0)
|
||||
|
||||
assert rounded.mode == "RGBA"
|
||||
assert rounded.size == (200, 50)
|
||||
|
||||
# Radius should be based on shorter side (50)
|
||||
# 30% of 50 = 15 pixels radius
|
||||
assert rounded.getpixel((0, 0))[3] == 0
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Tests for edge cases and boundary conditions"""
|
||||
|
||||
def test_1x1_image_rounded_corners(self):
|
||||
"""Test rounded corners on 1x1 image"""
|
||||
img = Image.new("RGB", (1, 1), color="white")
|
||||
result = apply_rounded_corners(img, 50.0)
|
||||
assert result.size == (1, 1)
|
||||
|
||||
def test_very_small_radius(self):
|
||||
"""Test with very small radius percentage"""
|
||||
img = Image.new("RGB", (100, 100), color="cyan")
|
||||
result = apply_rounded_corners(img, 0.1)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_shadow_with_transparent_image(self):
|
||||
"""Test shadow on fully transparent image"""
|
||||
img = Image.new("RGBA", (50, 50), (0, 0, 0, 0))
|
||||
result = apply_drop_shadow(img, expand=True)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_border_on_small_image(self):
|
||||
"""Test border on small image (larger than border)"""
|
||||
# Use a 10x10 image with 2px border (edge case with very small image)
|
||||
result = create_border_image(10, 10, 2)
|
||||
assert result.size == (10, 10)
|
||||
assert result.mode == "RGBA"
|
||||
|
||||
def test_styling_preserves_pixel_data(self):
|
||||
"""Test that styling preserves underlying pixel data"""
|
||||
# Create image with known pattern
|
||||
img = Image.new("RGB", (50, 50), (0, 0, 0))
|
||||
img.putpixel((25, 25), (255, 255, 255))
|
||||
|
||||
result = apply_rounded_corners(img, 5.0)
|
||||
|
||||
# Center white pixel should still be white (with alpha)
|
||||
pixel = result.getpixel((25, 25))
|
||||
assert pixel[0] == 255
|
||||
assert pixel[1] == 255
|
||||
assert pixel[2] == 255
|
||||
assert pixel[3] == 255 # Opaque in center
|
||||
Reference in New Issue
Block a user