development-hub/tests/test_undo_manager.py

334 lines
10 KiB
Python

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