"""Tests for the UndoManager class.""" import pytest from development_hub.views.dashboard.undo_manager import UndoAction, UndoManager class TestUndoAction: """Tests for UndoAction dataclass.""" def test_create_action(self): """Create a basic undo action.""" action = UndoAction( action_type="todo_toggle", data=("task text", "high", True, None), description="Toggle: task text" ) assert action.action_type == "todo_toggle" assert action.data == ("task text", "high", True, None) assert action.description == "Toggle: task text" def test_action_with_empty_data(self): """Action with empty data tuple.""" action = UndoAction( action_type="test", data=(), description="Empty action" ) assert action.data == () def test_action_with_complex_data(self): """Action with nested data structures.""" action = UndoAction( action_type="goal_edit", data=("old text", "new text", "high", True, False, "active"), description="Edit goal" ) assert len(action.data) == 6 class TestUndoManager: """Tests for UndoManager class.""" def test_initial_state(self): """New manager has empty stacks.""" manager = UndoManager() assert manager.can_undo() == False assert manager.can_redo() == False def test_push_action(self): """Push action enables undo.""" manager = UndoManager() action = UndoAction("test", ("data",), "Test action") manager.push_action(action) assert manager.can_undo() == True assert manager.can_redo() == False def test_push_clears_redo_stack(self): """New action clears redo stack.""" manager = UndoManager() # Setup: push, undo, verify redo available action1 = UndoAction("test1", ("data1",), "Action 1") manager.push_action(action1) # Register a handler that returns an action for redo manager.register_handler( "test", undo_callback=lambda d: UndoAction("test1", d, "Redo 1"), redo_callback=lambda d: UndoAction("test1", d, "Undo 1"), ) manager.undo() assert manager.can_redo() == True # Push new action should clear redo action2 = UndoAction("test2", ("data2",), "Action 2") manager.push_action(action2) assert manager.can_redo() == False def test_undo_returns_true_on_success(self): """Undo returns True when action was undone.""" manager = UndoManager() manager.register_handler( "test", undo_callback=lambda d: None, redo_callback=lambda d: None, ) action = UndoAction("test", ("data",), "Test") manager.push_action(action) result = manager.undo() assert result == True def test_undo_returns_false_when_empty(self): """Undo returns False when stack is empty.""" manager = UndoManager() result = manager.undo() assert result == False def test_redo_returns_true_on_success(self): """Redo returns True when action was redone.""" manager = UndoManager() manager.register_handler( "test", undo_callback=lambda d: UndoAction("test", d, "Redo"), redo_callback=lambda d: None, ) action = UndoAction("test", ("data",), "Test") manager.push_action(action) manager.undo() result = manager.redo() assert result == True def test_redo_returns_false_when_empty(self): """Redo returns False when stack is empty.""" manager = UndoManager() result = manager.redo() assert result == False def test_handler_registration(self): """Registered handlers are called for matching action types.""" manager = UndoManager() callback_called = [False] def undo_callback(data): callback_called[0] = True return None manager.register_handler( "todo", undo_callback=undo_callback, redo_callback=lambda d: None, ) action = UndoAction("todo_toggle", ("data",), "Toggle") manager.push_action(action) manager.undo() assert callback_called[0] == True def test_handler_prefix_matching(self): """Handler matches by prefix.""" manager = UndoManager() called_with = [None] def undo_callback(data): called_with[0] = data return None manager.register_handler( "goal", undo_callback=undo_callback, redo_callback=lambda d: None, ) # All these should match the "goal" prefix for action_type in ["goal_toggle", "goal_delete", "goal_edit"]: data = (action_type, "test") action = UndoAction(action_type, data, "Test") manager.push_action(action) manager.undo() assert called_with[0] == data def test_undo_redo_cycle(self): """Full undo/redo cycle preserves data.""" manager = UndoManager() undo_data = [] redo_data = [] def undo_callback(data): undo_data.append(data) return UndoAction("test", data, "Redo") def redo_callback(data): redo_data.append(data) return UndoAction("test", data, "Undo") manager.register_handler("test", undo_callback, redo_callback) original_data = ("text", "high", True) action = UndoAction("test", original_data, "Original") manager.push_action(action) # Undo manager.undo() assert undo_data[-1] == original_data # Redo manager.redo() assert redo_data[-1] == original_data def test_multiple_actions(self): """Multiple actions can be undone in order.""" manager = UndoManager() undone = [] def undo_callback(data): undone.append(data[0]) return UndoAction("test", data, "Redo") manager.register_handler( "test", undo_callback=undo_callback, redo_callback=lambda d: UndoAction("test", d, "Undo"), ) # Push 3 actions for i in range(3): action = UndoAction("test", (i,), f"Action {i}") manager.push_action(action) # Undo all 3 (should be LIFO order) manager.undo() manager.undo() manager.undo() assert undone == [2, 1, 0] def test_clear(self): """Clear empties both stacks.""" manager = UndoManager() manager.register_handler( "test", undo_callback=lambda d: UndoAction("test", d, "Redo"), redo_callback=lambda d: UndoAction("test", d, "Undo"), ) # Push and undo to have items in both stacks action = UndoAction("test", ("data",), "Test") manager.push_action(action) manager.undo() assert manager.can_undo() == False assert manager.can_redo() == True manager.clear() assert manager.can_undo() == False assert manager.can_redo() == False def test_last_action_description(self): """Get description of last undoable action.""" manager = UndoManager() assert manager.last_action_description() is None action1 = UndoAction("test1", ("data",), "First action") manager.push_action(action1) assert manager.last_action_description() == "First action" action2 = UndoAction("test2", ("data",), "Second action") manager.push_action(action2) assert manager.last_action_description() == "Second action" def test_no_handler_still_works(self): """Undo/redo work even without registered handler.""" manager = UndoManager() action = UndoAction("unhandled", ("data",), "No handler") manager.push_action(action) # Should not raise, just return True (action was popped) result = manager.undo() assert result == True assert manager.can_undo() == False class TestUndoManagerSignals: """Tests for UndoManager signal emissions.""" def test_state_changed_on_push(self, qtbot): """state_changed emits on push_action.""" manager = UndoManager() with qtbot.waitSignal(manager.state_changed, timeout=1000) as blocker: action = UndoAction("test", ("data",), "Test") manager.push_action(action) assert blocker.args == [True, False] # can_undo=True, can_redo=False def test_state_changed_on_undo(self, qtbot): """state_changed emits on undo.""" manager = UndoManager() manager.register_handler("test", lambda d: None, lambda d: None) action = UndoAction("test", ("data",), "Test") manager.push_action(action) with qtbot.waitSignal(manager.state_changed, timeout=1000) as blocker: manager.undo() assert blocker.args == [False, False] # No redo since handler returned None def test_action_performed_on_undo(self, qtbot): """action_performed emits on undo.""" manager = UndoManager() manager.register_handler("todo", lambda d: None, lambda d: None) action = UndoAction("todo_toggle", ("data",), "Toggle") manager.push_action(action) with qtbot.waitSignal(manager.action_performed, timeout=1000) as blocker: manager.undo() assert blocker.args == ["todo_toggle", True] # action_type, was_undo def test_action_performed_on_redo(self, qtbot): """action_performed emits on redo.""" manager = UndoManager() manager.register_handler( "todo", lambda d: UndoAction("todo_toggle", d, "Redo"), lambda d: None, ) action = UndoAction("todo_toggle", ("data",), "Toggle") manager.push_action(action) manager.undo() with qtbot.waitSignal(manager.action_performed, timeout=1000) as blocker: manager.redo() assert blocker.args == ["todo_toggle", False] # action_type, was_undo=False if __name__ == "__main__": pytest.main([__file__, "-v"])