pyPhotoAlbum/tests/test_template_manager.py
Duncan Tourolle 9ed8976885
All checks were successful
Python CI / test (push) Successful in 57s
Lint / lint (push) Successful in 1m6s
Tests / test (3.10) (push) Successful in 44s
Tests / test (3.11) (push) Successful in 44s
Tests / test (3.9) (push) Successful in 42s
trying to fix in built templates
2025-10-28 23:05:55 +01:00

693 lines
26 KiB
Python

"""
Unit tests for pyPhotoAlbum template management system
"""
import pytest
import tempfile
import json
from pathlib import Path
from pyPhotoAlbum.template_manager import Template, TemplateManager
from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.project import Page
class TestTemplate:
"""Tests for Template class"""
def test_initialization_default(self):
"""Test Template initialization with default values"""
template = Template()
assert template.name == "Untitled Template"
assert template.description == ""
assert template.page_size_mm == (210, 297)
assert len(template.elements) == 0
def test_initialization_with_parameters(self):
"""Test Template initialization with custom parameters"""
template = Template(
name="My Template",
description="Test template",
page_size_mm=(200, 280)
)
assert template.name == "My Template"
assert template.description == "Test template"
assert template.page_size_mm == (200, 280)
def test_add_element(self):
"""Test adding elements to template"""
template = Template()
placeholder = PlaceholderData(x=10, y=20, width=100, height=50)
template.add_element(placeholder)
assert len(template.elements) == 1
assert template.elements[0] == placeholder
def test_add_multiple_elements(self):
"""Test adding multiple elements"""
template = Template()
elem1 = PlaceholderData(x=10, y=20, width=100, height=50)
elem2 = TextBoxData(text_content="Test", x=30, y=40, width=150, height=60)
template.add_element(elem1)
template.add_element(elem2)
assert len(template.elements) == 2
assert elem1 in template.elements
assert elem2 in template.elements
def test_to_dict(self):
"""Test serialization to dictionary"""
template = Template(name="Test", description="Desc", page_size_mm=(200, 280))
placeholder = PlaceholderData(x=10, y=20, width=100, height=50)
template.add_element(placeholder)
data = template.to_dict()
assert data["name"] == "Test"
assert data["description"] == "Desc"
assert data["page_size_mm"] == (200, 280)
assert len(data["elements"]) == 1
assert data["elements"][0]["type"] == "placeholder"
def test_from_dict(self):
"""Test deserialization from dictionary"""
data = {
"name": "Loaded Template",
"description": "Test description",
"page_size_mm": [220, 300],
"elements": [
{
"type": "placeholder",
"position": (50, 60),
"size": (120, 80),
"placeholder_type": "image"
},
{
"type": "textbox",
"position": (70, 90),
"size": (140, 100),
"text_content": "Test text"
}
]
}
template = Template.from_dict(data)
assert template.name == "Loaded Template"
assert template.description == "Test description"
assert template.page_size_mm == (220, 300)
assert len(template.elements) == 2
assert isinstance(template.elements[0], PlaceholderData)
assert isinstance(template.elements[1], TextBoxData)
def test_from_dict_skips_image_elements(self):
"""Test that from_dict skips image elements"""
data = {
"name": "Test",
"elements": [
{"type": "image", "position": (10, 20), "size": (100, 50)},
{"type": "placeholder", "position": (30, 40), "size": (120, 60)}
]
}
template = Template.from_dict(data)
# Should only have the placeholder, not the image
assert len(template.elements) == 1
assert isinstance(template.elements[0], PlaceholderData)
def test_save_to_file(self, temp_dir):
"""Test saving template to file"""
template = Template(name="Save Test", description="Test save")
placeholder = PlaceholderData(x=10, y=20, width=100, height=50)
template.add_element(placeholder)
file_path = Path(temp_dir) / "test_template.json"
template.save_to_file(str(file_path))
# Verify file was created
assert file_path.exists()
# Verify content
with open(file_path, 'r') as f:
data = json.load(f)
assert data["name"] == "Save Test"
assert data["description"] == "Test save"
def test_load_from_file(self, temp_dir):
"""Test loading template from file"""
# Create a test file
data = {
"name": "Load Test",
"description": "Test load",
"page_size_mm": [210, 297],
"elements": [
{
"type": "placeholder",
"position": (10, 20),
"size": (100, 50),
"placeholder_type": "image"
}
]
}
file_path = Path(temp_dir) / "load_test.json"
with open(file_path, 'w') as f:
json.dump(data, f)
# Load template
template = Template.load_from_file(str(file_path))
assert template.name == "Load Test"
assert template.description == "Test load"
assert len(template.elements) == 1
class TestTemplateManager:
"""Tests for TemplateManager class"""
def test_initialization(self):
"""Test TemplateManager initialization"""
manager = TemplateManager()
assert manager.templates_dir is not None
assert isinstance(manager.templates_dir, Path)
def test_get_templates_directory(self):
"""Test getting templates directory"""
manager = TemplateManager()
templates_dir = manager._get_templates_directory()
assert templates_dir.name == "templates"
assert ".pyphotoalbum" in str(templates_dir)
def test_get_builtin_templates_directory(self):
"""Test getting built-in templates directory"""
manager = TemplateManager()
builtin_dir = manager._get_builtin_templates_directory()
assert builtin_dir.name == "templates"
assert "pyPhotoAlbum" in str(builtin_dir)
def test_list_templates_empty(self, tmp_path, monkeypatch):
"""Test listing templates when directory is empty"""
# Create temporary directories
user_dir = tmp_path / "user_templates"
builtin_dir = tmp_path / "builtin_templates"
user_dir.mkdir()
builtin_dir.mkdir()
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
monkeypatch.setattr(manager, '_get_builtin_templates_directory', lambda: builtin_dir)
templates = manager.list_templates()
assert templates == []
def test_list_templates_with_files(self, tmp_path, monkeypatch):
"""Test listing templates with template files"""
user_dir = tmp_path / "user_templates"
builtin_dir = tmp_path / "builtin_templates"
user_dir.mkdir()
builtin_dir.mkdir()
# Create user template
user_template = user_dir / "My_Template.json"
user_template.write_text('{"name": "My Template"}')
# Create built-in template
builtin_template = builtin_dir / "Grid_2x2.json"
builtin_template.write_text('{"name": "Grid 2x2"}')
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
monkeypatch.setattr(manager, '_get_builtin_templates_directory', lambda: builtin_dir)
templates = manager.list_templates()
assert "[Built-in] Grid_2x2" in templates
assert "My_Template" in templates
assert len(templates) == 2
def test_save_template(self, tmp_path, monkeypatch):
"""Test saving a template"""
user_dir = tmp_path / "user_templates"
user_dir.mkdir()
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
template = Template(name="Test Template")
manager.save_template(template)
# Verify file was created
template_file = user_dir / "Test Template.json"
assert template_file.exists()
def test_load_template_user(self, tmp_path, monkeypatch):
"""Test loading a user template"""
user_dir = tmp_path / "user_templates"
user_dir.mkdir()
# Create template file
data = {"name": "User Template", "description": "Test", "page_size_mm": [210, 297], "elements": []}
template_file = user_dir / "User Template.json"
with open(template_file, 'w') as f:
json.dump(data, f)
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
template = manager.load_template("User Template")
assert template.name == "User Template"
def test_load_template_builtin(self, tmp_path, monkeypatch):
"""Test loading a built-in template"""
builtin_dir = tmp_path / "builtin_templates"
builtin_dir.mkdir()
# Create built-in template file
data = {"name": "Grid 2x2", "description": "Built-in grid", "page_size_mm": [210, 297], "elements": []}
template_file = builtin_dir / "Grid 2x2.json"
with open(template_file, 'w') as f:
json.dump(data, f)
manager = TemplateManager()
monkeypatch.setattr(manager, '_get_builtin_templates_directory', lambda: builtin_dir)
template = manager.load_template("[Built-in] Grid 2x2")
assert template.name == "Grid 2x2"
def test_load_template_not_found(self, tmp_path, monkeypatch):
"""Test loading non-existent template raises error"""
user_dir = tmp_path / "user_templates"
user_dir.mkdir()
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
with pytest.raises(FileNotFoundError):
manager.load_template("NonExistent")
def test_delete_template(self, tmp_path, monkeypatch):
"""Test deleting a user template"""
user_dir = tmp_path / "user_templates"
user_dir.mkdir()
# Create template file
template_file = user_dir / "DeleteMe.json"
template_file.write_text('{"name": "DeleteMe"}')
manager = TemplateManager()
monkeypatch.setattr(manager, 'templates_dir', user_dir)
manager.delete_template("DeleteMe")
assert not template_file.exists()
def test_delete_builtin_template_raises_error(self):
"""Test deleting built-in template raises error"""
manager = TemplateManager()
with pytest.raises(PermissionError):
manager.delete_template("[Built-in] Grid_2x2")
def test_create_template_from_page(self):
"""Test creating template from a page"""
# Create a page with various elements
layout = PageLayout(width=210, height=297)
img = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
text = TextBoxData(text_content="Test", x=30, y=40, width=150, height=60)
placeholder = PlaceholderData(x=50, y=60, width=120, height=70)
layout.add_element(img)
layout.add_element(text)
layout.add_element(placeholder)
page = Page(layout=layout, page_number=1)
# Create template
manager = TemplateManager()
template = manager.create_template_from_page(
page,
name="Test Template",
description="Created from page"
)
assert template.name == "Test Template"
assert template.description == "Created from page"
assert len(template.elements) == 3
# Image should be converted to placeholder
assert isinstance(template.elements[0], PlaceholderData)
assert isinstance(template.elements[1], TextBoxData)
assert isinstance(template.elements[2], PlaceholderData)
def test_scale_template_elements_proportional(self):
"""Test scaling template elements proportionally"""
manager = TemplateManager()
# Create elements at 200x200 size
elem = PlaceholderData(x=50, y=50, width=100, height=100)
elements = [elem]
# Scale to 400x400 (2x scale)
scaled = manager.scale_template_elements(
elements,
from_size=(200, 200),
to_size=(400, 400),
scale_mode="proportional"
)
assert len(scaled) == 1
# With proportional scaling and centering
# scale = min(400/200, 400/200) = 2.0
# offset = (400 - 200*2) / 2 = 0
assert scaled[0].position == (100, 100) # 50 * 2 + 0
assert scaled[0].size == (200, 200) # 100 * 2
def test_scale_template_elements_stretch(self):
"""Test scaling template elements with stretch mode"""
manager = TemplateManager()
elem = PlaceholderData(x=50, y=50, width=100, height=100)
elements = [elem]
# Scale to 400x200 (2x width, 1x height)
scaled = manager.scale_template_elements(
elements,
from_size=(200, 200),
to_size=(400, 200),
scale_mode="stretch"
)
assert len(scaled) == 1
assert scaled[0].position == (100, 50) # 50 * 2, 50 * 1
assert scaled[0].size == (200, 100) # 100 * 2, 100 * 1
def test_scale_template_elements_center(self):
"""Test scaling template elements with center mode"""
manager = TemplateManager()
elem = PlaceholderData(x=50, y=50, width=100, height=100)
elements = [elem]
# Center in larger space without scaling
scaled = manager.scale_template_elements(
elements,
from_size=(200, 200),
to_size=(400, 400),
scale_mode="center"
)
assert len(scaled) == 1
# offset = (400 - 200) / 2 = 100
assert scaled[0].position == (150, 150) # 50 + 100
assert scaled[0].size == (100, 100) # No scaling
def test_scale_template_preserves_properties(self):
"""Test that scaling preserves element properties"""
manager = TemplateManager()
elem = PlaceholderData(x=50, y=50, width=100, height=100)
elem.rotation = 45
elem.z_index = 5
elem.placeholder_type = "image"
scaled = manager.scale_template_elements(
[elem],
from_size=(200, 200),
to_size=(400, 400),
scale_mode="proportional"
)
assert scaled[0].rotation == 45
assert scaled[0].z_index == 5
assert scaled[0].placeholder_type == "image"
def test_apply_template_to_page_replace(self):
"""Test applying template with replace mode"""
manager = TemplateManager()
# Create template
template = Template(page_size_mm=(200, 200))
template.add_element(PlaceholderData(x=10, y=20, width=80, height=60))
# Create page with existing content
layout = PageLayout(width=200, height=200)
layout.add_element(ImageData(x=100, y=100, width=50, height=50))
page = Page(layout=layout, page_number=1)
# Apply template
manager.apply_template_to_page(template, page, mode="replace")
# Page should have only template elements
assert len(page.layout.elements) == 1
assert isinstance(page.layout.elements[0], PlaceholderData)
def test_apply_template_to_page_reflow(self):
"""Test applying template with reflow mode"""
manager = TemplateManager()
# Create template with 2 placeholders
template = Template(page_size_mm=(200, 200))
template.add_element(PlaceholderData(x=10, y=20, width=80, height=60))
template.add_element(PlaceholderData(x=100, y=100, width=80, height=60))
# Create page with 1 image
layout = PageLayout(width=200, height=200)
img = ImageData(image_path="test.jpg", x=50, y=50, width=50, height=50)
layout.add_element(img)
page = Page(layout=layout, page_number=1)
# Apply template with reflow
manager.apply_template_to_page(template, page, mode="reflow")
# Should have 1 image (reflowed) + 1 placeholder
assert len(page.layout.elements) == 2
# First should be the reflowed image
assert isinstance(page.layout.elements[0], ImageData)
# Second should be placeholder (no image to fill it)
assert isinstance(page.layout.elements[1], PlaceholderData)
def test_create_page_from_template_default_size(self):
"""Test creating page from template with default size"""
manager = TemplateManager()
# Create template
template = Template(page_size_mm=(210, 297))
template.add_element(PlaceholderData(x=10, y=20, width=100, height=50))
# Create page
page = manager.create_page_from_template(template, page_number=5)
assert page.page_number == 5
assert page.layout.size == (210, 297)
assert len(page.layout.elements) == 1
assert isinstance(page.layout.elements[0], PlaceholderData)
def test_create_page_from_template_custom_size(self):
"""Test creating page from template with custom size"""
manager = TemplateManager()
# Create template at 200x200
template = Template(page_size_mm=(200, 200))
template.add_element(PlaceholderData(x=50, y=50, width=100, height=100))
# Create page at 400x400
page = manager.create_page_from_template(
template,
page_number=1,
target_size_mm=(400, 400),
scale_mode="proportional"
)
assert page.layout.size == (400, 400)
assert len(page.layout.elements) == 1
# Element should be scaled
assert page.layout.elements[0].size == (200, 200) # 100 * 2
def test_scale_with_textbox_preserves_font_settings(self):
"""Test that scaling preserves text box font settings"""
manager = TemplateManager()
font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
text = TextBoxData(
text_content="Test",
font_settings=font_settings,
x=50,
y=50,
width=100,
height=50
)
scaled = manager.scale_template_elements(
[text],
from_size=(200, 200),
to_size=(400, 400),
scale_mode="proportional"
)
assert scaled[0].text_content == "Test"
assert scaled[0].font_settings == font_settings
assert scaled[0].alignment == text.alignment
def test_grid_2x2_stretch_to_square_page(self):
"""Test Grid_2x2 template applied to square page with stretch mode"""
manager = TemplateManager()
# Create a 2x2 grid template at 210x210mm (margin-less, fills entire space)
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
# 4 cells: each 105 x 105mm (half of 210mm)
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=0, y=105, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=105, width=105, height=105))
# Apply to same size page with stretch mode and 2.5% margin
layout = PageLayout(width=210, height=210)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=2.5
)
# With 2.5% margin on 210mm page: margin = 5.25mm, content area = 199.5mm
# Template is 210mm, so scale = 199.5 / 210 = 0.95
# Each element should scale by 0.95 and be offset by margin
assert len(page.layout.elements) == 4
# Check first element (top-left)
elem = page.layout.elements[0]
scale = 199.5 / 210.0 # 0.95
expected_x = 0 * scale + 5.25 # 0 + 5.25 = 5.25
expected_y = 0 * scale + 5.25 # 0 + 5.25 = 5.25
expected_width = 105 * scale # 99.75
expected_height = 105 * scale # 99.75
assert abs(elem.position[0] - expected_x) < 0.1
assert abs(elem.position[1] - expected_y) < 0.1
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
def test_grid_2x2_stretch_to_a4_page(self):
"""Test Grid_2x2 template applied to A4 page with stretch mode"""
manager = TemplateManager()
# Create Grid_2x2 template (210x210mm, margin-less)
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=0, width=105, height=105))
template.add_element(PlaceholderData(x=0, y=105, width=105, height=105))
template.add_element(PlaceholderData(x=105, y=105, width=105, height=105))
# Apply to A4 page (210x297mm) with stretch mode and 2.5% margin
layout = PageLayout(width=210, height=297)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=2.5
)
# With 2.5% margin: x_margin = 5.25mm, y_margin = 7.425mm
# Content area: 199.5 x 282.15mm
# Scale: x = 199.5/210 = 0.95, y = 282.15/210 = 1.3436
assert len(page.layout.elements) == 4
# First element should stretch
elem = page.layout.elements[0]
scale_x = 199.5 / 210.0
scale_y = 282.15 / 210.0
expected_x = 0 * scale_x + 5.25 # 5.25
expected_y = 0 * scale_y + 7.425 # 7.425
expected_width = 105 * scale_x # 99.75
expected_height = 105 * scale_y # 141.075
assert abs(elem.position[0] - expected_x) < 0.1
assert abs(elem.position[1] - expected_y) < 0.1
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
def test_grid_2x2_with_different_margins(self):
"""Test Grid_2x2 template with different margin percentages"""
manager = TemplateManager()
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
# Test with 0% margin
layout = PageLayout(width=210, height=210)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="stretch",
margin_percent=0.0
)
# With 0% margin, template fills entire page (scale = 1.0, offset = 0)
elem = page.layout.elements[0]
assert abs(elem.position[0] - 0.0) < 0.1
assert abs(elem.position[1] - 0.0) < 0.1
assert abs(elem.size[0] - 105.0) < 0.1
# Test with 5% margin
layout2 = PageLayout(width=210, height=210)
page2 = Page(layout=layout2, page_number=1)
manager.apply_template_to_page(
template, page2,
mode="replace",
scale_mode="stretch",
margin_percent=5.0
)
# With 5% margin: margin = 10.5mm, content = 189mm, scale = 189/210 = 0.9
elem2 = page2.layout.elements[0]
assert abs(elem2.position[0] - 10.5) < 0.1
assert abs(elem2.position[1] - 10.5) < 0.1
assert abs(elem2.size[0] - (105 * 0.9)) < 0.1
def test_grid_2x2_proportional_mode(self):
"""Test Grid_2x2 template with proportional scaling"""
manager = TemplateManager()
template = Template(name="Grid_2x2", page_size_mm=(210, 210))
template.add_element(PlaceholderData(x=0, y=0, width=105, height=105))
# Apply to rectangular page with proportional mode
layout = PageLayout(width=210, height=297)
page = Page(layout=layout, page_number=1)
manager.apply_template_to_page(
template, page,
mode="replace",
scale_mode="proportional",
margin_percent=2.5
)
# With proportional mode on 210x297 page:
# Content area: 199.5 x 282.15mm
# Template: 210 x 210mm
# Scale = min(199.5/210, 282.15/210) = 0.95 (uniform)
# Content is centered on page
elem = page.layout.elements[0]
scale = 199.5 / 210.0
# Should be scaled uniformly
expected_width = 105 * scale # 99.75
expected_height = 105 * scale # 99.75
assert abs(elem.size[0] - expected_width) < 0.1
assert abs(elem.size[1] - expected_height) < 0.1
# Width should equal height (uniform scaling)
assert abs(elem.size[0] - elem.size[1]) < 0.1