""" Tests for Command pattern implementation """ import pytest from unittest.mock import Mock, MagicMock from pyPhotoAlbum.commands import ( AddElementCommand, DeleteElementCommand, MoveElementCommand, ResizeElementCommand, RotateElementCommand, AdjustImageCropCommand, AlignElementsCommand, ResizeElementsCommand, ChangeZOrderCommand, StateChangeCommand, CommandHistory, _normalize_asset_path ) from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.project import Project, Page class TestNormalizeAssetPath: """Test _normalize_asset_path helper function""" def test_normalize_absolute_path(self): """Test converting absolute path to relative""" mock_manager = Mock() mock_manager.project_folder = "/project" result = _normalize_asset_path("/project/assets/image.jpg", mock_manager) assert result == "assets/image.jpg" def test_normalize_relative_path_unchanged(self): """Test relative path stays unchanged""" mock_manager = Mock() mock_manager.project_folder = "/project" result = _normalize_asset_path("assets/image.jpg", mock_manager) assert result == "assets/image.jpg" def test_normalize_no_asset_manager(self): """Test with no asset manager returns unchanged""" result = _normalize_asset_path("/path/to/image.jpg", None) assert result == "/path/to/image.jpg" def test_normalize_empty_path(self): """Test with empty path""" mock_manager = Mock() result = _normalize_asset_path("", mock_manager) assert result == "" class TestAddElementCommand: """Test AddElementCommand""" def test_add_element_execute(self): """Test adding element to layout""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) assert len(layout.elements) == 0 cmd.execute() assert len(layout.elements) == 1 assert element in layout.elements def test_add_element_undo(self): """Test undoing element addition""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) cmd.execute() assert len(layout.elements) == 1 cmd.undo() assert len(layout.elements) == 0 def test_add_element_redo(self): """Test redoing element addition""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) cmd.execute() cmd.undo() cmd.redo() assert len(layout.elements) == 1 assert element in layout.elements def test_add_element_serialization(self): """Test serializing add element command""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) cmd.execute() data = cmd.serialize() assert data['type'] == 'add_element' assert 'element' in data assert data['executed'] is True def test_add_element_with_asset_manager(self): """Test add element with asset manager reference""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) mock_asset_manager = Mock() mock_asset_manager.project_folder = "/project" mock_asset_manager.acquire_reference = Mock() cmd = AddElementCommand(layout, element, asset_manager=mock_asset_manager) # Should acquire reference on creation assert mock_asset_manager.acquire_reference.called class TestDeleteElementCommand: """Test DeleteElementCommand""" def test_delete_element_execute(self): """Test deleting element from layout""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) layout.add_element(element) cmd = DeleteElementCommand(layout, element) cmd.execute() assert len(layout.elements) == 0 assert element not in layout.elements def test_delete_element_undo(self): """Test undoing element deletion""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) layout.add_element(element) cmd = DeleteElementCommand(layout, element) cmd.execute() cmd.undo() assert len(layout.elements) == 1 assert element in layout.elements def test_delete_element_serialization(self): """Test serializing delete element command""" layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) layout.add_element(element) cmd = DeleteElementCommand(layout, element) data = cmd.serialize() assert data['type'] == 'delete_element' assert 'element' in data class TestMoveElementCommand: """Test MoveElementCommand""" def test_move_element_execute(self): """Test moving element""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200)) cmd.execute() assert element.position == (200, 200) def test_move_element_undo(self): """Test undoing element move""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200)) cmd.execute() cmd.undo() assert element.position == (100, 100) def test_move_element_serialization(self): """Test serializing move command""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = MoveElementCommand(element, old_position=(100, 100), new_position=(200, 200)) data = cmd.serialize() assert data['type'] == 'move_element' assert data['old_position'] == (100, 100) assert data['new_position'] == (200, 200) class TestResizeElementCommand: """Test ResizeElementCommand""" def test_resize_element_execute(self): """Test resizing element""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = ResizeElementCommand( element, old_position=(100, 100), old_size=(200, 150), new_position=(100, 100), new_size=(300, 225) ) cmd.execute() assert element.size == (300, 225) def test_resize_element_undo(self): """Test undoing element resize""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = ResizeElementCommand( element, old_position=(100, 100), old_size=(200, 150), new_position=(100, 100), new_size=(300, 225) ) cmd.execute() cmd.undo() assert element.size == (200, 150) def test_resize_changes_position(self): """Test resize that also changes position""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = ResizeElementCommand( element, old_position=(100, 100), old_size=(200, 150), new_position=(90, 90), new_size=(220, 165) ) cmd.execute() assert element.position == (90, 90) assert element.size == (220, 165) class TestRotateElementCommand: """Test RotateElementCommand""" def test_rotate_element_execute(self): """Test rotating element""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) element.rotation = 0 cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90) cmd.execute() assert element.rotation == 90 def test_rotate_element_undo(self): """Test undoing element rotation""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) element.rotation = 0 cmd = RotateElementCommand(element, old_rotation=0, new_rotation=90) cmd.execute() cmd.undo() assert element.rotation == 0 def test_rotate_element_serialization(self): """Test serializing rotate command""" element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = RotateElementCommand(element, old_rotation=0, new_rotation=45) data = cmd.serialize() assert data['type'] == 'rotate_element' assert data['old_rotation'] == 0 assert data['new_rotation'] == 45 class TestAdjustImageCropCommand: """Test AdjustImageCropCommand""" def test_adjust_crop_execute(self): """Test adjusting image crop""" element = ImageData( image_path="/test.jpg", x=100, y=100, width=200, height=150, crop_info={'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0} ) new_crop = {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8} cmd = AdjustImageCropCommand( element, old_crop_info=element.crop_info.copy(), new_crop_info=new_crop ) cmd.execute() assert element.crop_info == new_crop def test_adjust_crop_undo(self): """Test undoing crop adjustment""" old_crop = {'x': 0.0, 'y': 0.0, 'width': 1.0, 'height': 1.0} element = ImageData( image_path="/test.jpg", x=100, y=100, width=200, height=150, crop_info=old_crop.copy() ) new_crop = {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8} cmd = AdjustImageCropCommand(element, old_crop_info=old_crop, new_crop_info=new_crop) cmd.execute() cmd.undo() assert element.crop_info == old_crop class TestAlignElementsCommand: """Test AlignElementsCommand""" def test_align_elements_execute(self): """Test aligning elements""" element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=150, width=100, height=100) # Set new positions before creating command element2.position = (100, 150) # Align left # Command expects list of (element, old_position) tuples changes = [(element1, (100, 100)), (element2, (200, 150))] cmd = AlignElementsCommand(changes) cmd.execute() # Execute does nothing (positions already set), check they remain assert element1.position == (100, 100) assert element2.position == (100, 150) def test_align_elements_undo(self): """Test undoing alignment""" element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=150, width=100, height=100) # Set new positions before creating command element2.position = (100, 150) # Align left # Command expects list of (element, old_position) tuples changes = [(element1, (100, 100)), (element2, (200, 150))] cmd = AlignElementsCommand(changes) cmd.execute() cmd.undo() # Should restore old positions assert element1.position == (100, 100) assert element2.position == (200, 150) class TestResizeElementsCommand: """Test ResizeElementsCommand""" def test_resize_elements_execute(self): """Test resizing multiple elements""" element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=150, height=150) # Set new sizes before creating command element1.size = (200, 200) element2.size = (300, 300) # Command expects list of (element, old_position, old_size) tuples changes = [ (element1, (100, 100), (100, 100)), (element2, (200, 200), (150, 150)) ] cmd = ResizeElementsCommand(changes) cmd.execute() # Execute does nothing (sizes already set), check they remain assert element1.size == (200, 200) assert element2.size == (300, 300) def test_resize_elements_undo(self): """Test undoing multiple element resize""" element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=150, height=150) # Set new sizes before creating command element1.size = (200, 200) element2.size = (300, 300) # Command expects list of (element, old_position, old_size) tuples changes = [ (element1, (100, 100), (100, 100)), (element2, (200, 200), (150, 150)) ] cmd = ResizeElementsCommand(changes) cmd.execute() cmd.undo() # Should restore old sizes assert element1.size == (100, 100) assert element2.size == (150, 150) class TestChangeZOrderCommand: """Test ChangeZOrderCommand""" def test_change_zorder_execute(self): """Test changing z-order""" layout = PageLayout(width=210, height=297) element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=120, y=120, width=100, height=100) layout.add_element(element1) layout.add_element(element2) # Move element1 to front (swap order) cmd = ChangeZOrderCommand(layout, element1, 0, 1) cmd.execute() assert layout.elements[1] == element1 def test_change_zorder_undo(self): """Test undoing z-order change""" layout = PageLayout(width=210, height=297) element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=120, y=120, width=100, height=100) layout.add_element(element1) layout.add_element(element2) cmd = ChangeZOrderCommand(layout, element1, 0, 1) cmd.execute() cmd.undo() assert layout.elements[0] == element1 class TestStateChangeCommand: """Test StateChangeCommand for generic state changes""" def test_state_change_undo(self): """Test undoing state change""" element = TextBoxData( text_content="Old Text", x=100, y=100, width=200, height=100 ) # Define restore function def restore_state(state): element.text_content = state['text_content'] old_state = {'text_content': 'Old Text'} new_state = {'text_content': 'New Text'} # Apply new state first element.text_content = 'New Text' cmd = StateChangeCommand( description="Change text", restore_func=restore_state, before_state=old_state, after_state=new_state ) # Undo should restore old state cmd.undo() assert element.text_content == 'Old Text' def test_state_change_redo(self): """Test redoing state change""" element = TextBoxData( text_content="Old Text", x=100, y=100, width=200, height=100 ) # Define restore function def restore_state(state): element.text_content = state['text_content'] old_state = {'text_content': 'Old Text'} new_state = {'text_content': 'New Text'} # Apply new state first element.text_content = 'New Text' cmd = StateChangeCommand( description="Change text", restore_func=restore_state, before_state=old_state, after_state=new_state ) # Undo then redo cmd.undo() assert element.text_content == 'Old Text' cmd.redo() assert element.text_content == 'New Text' def test_state_change_serialization(self): """Test serializing state change command""" def restore_func(state): pass cmd = StateChangeCommand( description="Test operation", restore_func=restore_func, before_state={'test': 'before'}, after_state={'test': 'after'} ) data = cmd.serialize() assert data['type'] == 'state_change' assert data['description'] == 'Test operation' class TestCommandHistory: """Test CommandHistory for undo/redo management""" def test_history_execute_command(self): """Test executing command through history""" history = CommandHistory() layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) history.execute(cmd) assert len(layout.elements) == 1 assert history.can_undo() assert not history.can_redo() def test_history_undo(self): """Test undo through history""" history = CommandHistory() layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) history.execute(cmd) history.undo() assert len(layout.elements) == 0 assert not history.can_undo() assert history.can_redo() def test_history_redo(self): """Test redo through history""" history = CommandHistory() layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) cmd = AddElementCommand(layout, element) history.execute(cmd) history.undo() history.redo() assert len(layout.elements) == 1 assert history.can_undo() assert not history.can_redo() def test_history_multiple_commands(self): """Test history with multiple commands""" history = CommandHistory() layout = PageLayout(width=210, height=297) element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=100, height=100) history.execute(AddElementCommand(layout, element1)) history.execute(AddElementCommand(layout, element2)) assert len(layout.elements) == 2 history.undo() assert len(layout.elements) == 1 history.undo() assert len(layout.elements) == 0 def test_history_clears_redo_on_new_command(self): """Test that new command clears redo stack""" history = CommandHistory() layout = PageLayout(width=210, height=297) element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100) element2 = ImageData(image_path="/test2.jpg", x=200, y=200, width=100, height=100) history.execute(AddElementCommand(layout, element1)) history.undo() assert history.can_redo() # Execute new command should clear redo stack history.execute(AddElementCommand(layout, element2)) assert not history.can_redo() def test_history_clear(self): """Test clearing history""" history = CommandHistory() layout = PageLayout(width=210, height=297) element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) history.execute(AddElementCommand(layout, element)) history.clear() assert not history.can_undo() assert not history.can_redo() def test_history_max_size(self): """Test history respects max size limit""" history = CommandHistory(max_history=3) layout = PageLayout(width=210, height=297) for i in range(5): element = ImageData(image_path=f"/test{i}.jpg", x=i*10, y=i*10, width=100, height=100) history.execute(AddElementCommand(layout, element)) # Should only have 3 commands in history (max_history) undo_count = 0 while history.can_undo(): history.undo() undo_count += 1 assert undo_count == 3