using Sandbox.Helpers; using System; using System.Collections.Generic; namespace EditorTests; [TestClass] public class UndoSystemTest { /// /// Undo and redo on a brand new system should fail gracefully. /// [TestMethod] public void EmptySystemReturnsFalse() { var undo = new UndoSystem(); Assert.IsFalse( undo.Undo() ); Assert.IsFalse( undo.Redo() ); Assert.AreEqual( 0, undo.Back.Count ); Assert.AreEqual( 0, undo.Forward.Count ); } /// /// Inserting an entry should push it onto the back stack and fill in the metadata. /// [TestMethod] public void InsertPushesOntoBackStack() { var undo = new UndoSystem(); var entry = undo.Insert( "My Change", () => { } ); Assert.IsNotNull( entry ); Assert.AreEqual( "My Change", entry.Name ); Assert.AreEqual( 1, undo.Back.Count ); Assert.AreEqual( 0, undo.Forward.Count ); Assert.AreSame( entry, undo.Back.Peek() ); } /// /// Undo should invoke the undo action, move the entry to the forward stack and /// raise . /// [TestMethod] public void UndoInvokesActionAndMovesEntryForward() { var undo = new UndoSystem(); var value = 1; UndoSystem.Entry undoneEntry = null; undo.OnUndo = e => undoneEntry = e; var entry = undo.Insert( "Set Value", () => value = 0, () => value = 1 ); Assert.IsTrue( undo.Undo() ); Assert.AreEqual( 0, value ); Assert.AreEqual( 0, undo.Back.Count ); Assert.AreEqual( 1, undo.Forward.Count ); Assert.AreSame( entry, undoneEntry ); } /// /// Redo should invoke the redo action, move the entry back onto the back stack and /// raise . /// [TestMethod] public void RedoInvokesActionAndMovesEntryBack() { var undo = new UndoSystem(); var value = 1; UndoSystem.Entry redoneEntry = null; undo.OnRedo = e => redoneEntry = e; var entry = undo.Insert( "Set Value", () => value = 0, () => value = 1 ); Assert.IsTrue( undo.Undo() ); Assert.IsTrue( undo.Redo() ); Assert.AreEqual( 1, value ); Assert.AreEqual( 1, undo.Back.Count ); Assert.AreEqual( 0, undo.Forward.Count ); Assert.AreSame( entry, redoneEntry ); } /// /// Entries should undo in reverse insertion order and redo in insertion order. /// [TestMethod] public void UndoRedoRoundTripRunsInOrder() { var undo = new UndoSystem(); var order = new List(); undo.Insert( "First", () => order.Add( "undo first" ), () => order.Add( "redo first" ) ); undo.Insert( "Second", () => order.Add( "undo second" ), () => order.Add( "redo second" ) ); Assert.IsTrue( undo.Undo() ); Assert.IsTrue( undo.Undo() ); Assert.IsFalse( undo.Undo() ); Assert.IsTrue( undo.Redo() ); Assert.IsTrue( undo.Redo() ); Assert.IsFalse( undo.Redo() ); CollectionAssert.AreEqual( new[] { "undo second", "undo first", "redo first", "redo second" }, order ); } /// /// Inserting a new entry should wipe the forward stack, so you can't redo into a /// state that no longer makes sense. /// [TestMethod] public void InsertClearsForwardStack() { var undo = new UndoSystem(); undo.Insert( "First", () => { } ); Assert.IsTrue( undo.Undo() ); Assert.AreEqual( 1, undo.Forward.Count ); undo.Insert( "Second", () => { } ); Assert.AreEqual( 0, undo.Forward.Count ); Assert.IsFalse( undo.Redo() ); } /// /// An undo action that throws shouldn't propagate - the entry still moves to the /// forward stack and the undo counts as handled. /// [TestMethod] public void UndoSwallowsExceptionFromAction() { var undo = new UndoSystem(); undo.Insert( "Broken", () => throw new InvalidOperationException( "boom" ) ); Assert.IsTrue( undo.Undo() ); Assert.AreEqual( 0, undo.Back.Count ); Assert.AreEqual( 1, undo.Forward.Count ); } /// /// A locked entry acts as a barrier - undoing it reports failure, doesn't raise /// and the entry stays on the back stack. Note the /// undo action itself still runs each attempt. /// [TestMethod] public void LockedEntryStaysOnBackStack() { var undo = new UndoSystem(); var invoked = 0; var undoEvents = 0; undo.OnUndo = _ => undoEvents++; var entry = undo.Insert( "Barrier", () => invoked++ ); entry.Locked = true; Assert.IsFalse( undo.Undo() ); Assert.IsFalse( undo.Undo() ); Assert.AreEqual( 2, invoked ); Assert.AreEqual( 0, undoEvents ); Assert.AreEqual( 1, undo.Back.Count ); Assert.AreEqual( 0, undo.Forward.Count ); } /// /// Entries with null actions should still flow between the stacks without throwing. /// [TestMethod] public void NullActionsAreTolerated() { var undo = new UndoSystem(); undo.Insert( "No Actions", null, null ); Assert.IsTrue( undo.Undo() ); Assert.IsTrue( undo.Redo() ); } /// /// should clear all history. /// [TestMethod] public void InitializeClearsHistory() { var undo = new UndoSystem(); undo.Insert( "First", () => { } ); undo.Insert( "Second", () => { } ); Assert.IsTrue( undo.Undo() ); undo.Initialize(); Assert.AreEqual( 0, undo.Back.Count ); Assert.AreEqual( 0, undo.Forward.Count ); Assert.IsFalse( undo.Undo() ); Assert.IsFalse( undo.Redo() ); } }