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() );
}
}