334 lines
10 KiB
Python
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"])
|