""" 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