| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| goog.provide('goog.editor.plugins.UndoRedoManagerTest'); |
| goog.setTestOnly('goog.editor.plugins.UndoRedoManagerTest'); |
| |
| goog.require('goog.editor.plugins.UndoRedoManager'); |
| goog.require('goog.editor.plugins.UndoRedoState'); |
| goog.require('goog.events'); |
| goog.require('goog.testing.StrictMock'); |
| goog.require('goog.testing.jsunit'); |
| |
| var mockState1; |
| var mockState2; |
| var mockState3; |
| var states; |
| var manager; |
| var stateChangeCount; |
| var beforeUndoCount; |
| var beforeRedoCount; |
| var preventDefault; |
| |
| function setUp() { |
| manager = new goog.editor.plugins.UndoRedoManager(); |
| stateChangeCount = 0; |
| goog.events.listen(manager, |
| goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE, |
| function() { |
| stateChangeCount++; |
| }); |
| |
| beforeUndoCount = 0; |
| preventDefault = false; |
| goog.events.listen(manager, |
| goog.editor.plugins.UndoRedoManager.EventType.BEFORE_UNDO, |
| function(e) { |
| beforeUndoCount++; |
| if (preventDefault) { |
| e.preventDefault(); |
| } |
| }); |
| |
| beforeRedoCount = 0; |
| goog.events.listen(manager, |
| goog.editor.plugins.UndoRedoManager.EventType.BEFORE_REDO, |
| function(e) { |
| beforeRedoCount++; |
| if (preventDefault) { |
| e.preventDefault(); |
| } |
| }); |
| |
| mockState1 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState); |
| mockState2 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState); |
| mockState3 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState); |
| states = [mockState1, mockState2, mockState3]; |
| |
| mockState1.equals = mockState2.equals = mockState3.equals = function(state) { |
| return this == state; |
| }; |
| |
| mockState1.isAsynchronous = mockState2.isAsynchronous = |
| mockState3.isAsynchronous = function() { |
| return false; |
| }; |
| } |
| |
| |
| function tearDown() { |
| goog.events.removeAll(manager); |
| manager.dispose(); |
| } |
| |
| |
| /** |
| * Adds all the mock states to the undo-redo manager. |
| */ |
| function addStatesToManager() { |
| manager.addState(states[0]); |
| |
| for (var i = 1; i < states.length; i++) { |
| var state = states[i]; |
| manager.addState(state); |
| } |
| |
| stateChangeCount = 0; |
| } |
| |
| |
| /** |
| * Resets all mock states so that they are ready for testing. |
| */ |
| function resetStates() { |
| for (var i = 0; i < states.length; i++) { |
| states[i].$reset(); |
| } |
| } |
| |
| |
| function testSetMaxUndoDepth() { |
| manager.setMaxUndoDepth(2); |
| addStatesToManager(); |
| assertArrayEquals('Undo stack must contain only the two most recent states.', |
| [mockState2, mockState3], manager.undoStack_); |
| } |
| |
| |
| function testAddState() { |
| var stateAddedCount = 0; |
| goog.events.listen(manager, |
| goog.editor.plugins.UndoRedoManager.EventType.STATE_ADDED, |
| function() { |
| stateAddedCount++; |
| }); |
| |
| manager.addState(mockState1); |
| assertArrayEquals('Undo stack must contain added state.', |
| [mockState1], manager.undoStack_); |
| assertEquals('Manager must dispatch one state change event on ' + |
| 'undo stack 0->1 transition.', 1, stateChangeCount); |
| assertEquals('State added must have dispatched once.', 1, stateAddedCount); |
| mockState1.$reset(); |
| |
| // Test adding same state twice. |
| manager.addState(mockState1); |
| assertArrayEquals('Undo stack must not contain two equal, sequential states.', |
| [mockState1], manager.undoStack_); |
| assertEquals('Manager must not dispatch state change event when nothing is ' + |
| 'added to the stack.', 1, stateChangeCount); |
| assertEquals('State added must have dispatched once.', 1, stateAddedCount); |
| |
| // Test adding a second state. |
| manager.addState(mockState2); |
| assertArrayEquals('Undo stack must contain both states.', |
| [mockState1, mockState2], manager.undoStack_); |
| assertEquals('Manager must not dispatch state change event when second ' + |
| 'state is added to the stack.', 1, stateChangeCount); |
| assertEquals('State added must have dispatched twice.', 2, stateAddedCount); |
| |
| // Test adding a state when there is state on the redo stack. |
| manager.undo(); |
| assertEquals('Manager must dispatch state change when redo stack goes to 1.', |
| 2, stateChangeCount); |
| |
| manager.addState(mockState3); |
| assertArrayEquals('Undo stack must contain states 1 and 3.', |
| [mockState1, mockState3], manager.undoStack_); |
| assertEquals('Manager must dispatch state change event when redo stack ' + |
| 'goes to zero.', 3, stateChangeCount); |
| assertEquals('State added must have dispatched three times.', |
| 3, stateAddedCount); |
| } |
| |
| |
| function testHasState() { |
| assertFalse('New manager must have no undo state.', manager.hasUndoState()); |
| assertFalse('New manager must have no redo state.', manager.hasRedoState()); |
| |
| manager.addState(mockState1); |
| assertTrue('Manager must have only undo state.', manager.hasUndoState()); |
| assertFalse('Manager must have no redo state.', manager.hasRedoState()); |
| |
| manager.undo(); |
| assertFalse('Manager must have no undo state.', manager.hasUndoState()); |
| assertTrue('Manager must have only redo state.', manager.hasRedoState()); |
| } |
| |
| |
| function testClearHistory() { |
| addStatesToManager(); |
| manager.undo(); |
| stateChangeCount = 0; |
| |
| manager.clearHistory(); |
| assertFalse('Undo stack must be empty.', manager.hasUndoState()); |
| assertFalse('Redo stack must be empty.', manager.hasRedoState()); |
| assertEquals('State change count must be 1 after clear history.', |
| 1, stateChangeCount); |
| |
| manager.clearHistory(); |
| assertEquals('Repeated clearHistory must not change state change count.', |
| 1, stateChangeCount); |
| } |
| |
| |
| function testUndo() { |
| addStatesToManager(); |
| |
| mockState3.undo(); |
| mockState3.$replay(); |
| manager.undo(); |
| assertEquals('Adding first item to redo stack must dispatch state change.', |
| 1, stateChangeCount); |
| assertEquals('Undo must cause before action to dispatch', |
| 1, beforeUndoCount); |
| mockState3.$verify(); |
| |
| preventDefault = true; |
| mockState2.$replay(); |
| manager.undo(); |
| assertEquals('No stack transitions between 0 and 1, must not dispatch ' + |
| 'state change.', 1, stateChangeCount); |
| assertEquals('Undo must cause before action to dispatch', |
| 2, beforeUndoCount); |
| mockState2.$verify(); // Verify that undo was prevented. |
| |
| preventDefault = false; |
| mockState1.undo(); |
| mockState1.$replay(); |
| manager.undo(); |
| assertEquals('Doing last undo operation must dispatch state change.', |
| 2, stateChangeCount); |
| assertEquals('Undo must cause before action to dispatch', |
| 3, beforeUndoCount); |
| mockState1.$verify(); |
| } |
| |
| |
| function testUndo_Asynchronous() { |
| // Using a stub instead of a mock here so that the state can behave as an |
| // EventTarget and dispatch events. |
| var stubState = new goog.editor.plugins.UndoRedoState(true); |
| var undoCalled = false; |
| stubState.undo = function() { |
| undoCalled = true; |
| }; |
| stubState.redo = goog.nullFunction; |
| stubState.equals = function() { |
| return false; |
| }; |
| |
| manager.addState(mockState2); |
| manager.addState(mockState1); |
| manager.addState(stubState); |
| |
| manager.undo(); |
| assertTrue('undoCalled must be true (undo must be called).', undoCalled); |
| assertEquals('Undo must cause before action to dispatch', |
| 1, beforeUndoCount); |
| |
| // Calling undo shouldn't actually undo since the first async undo hasn't |
| // fired an event yet. |
| mockState1.$replay(); |
| manager.undo(); |
| mockState1.$verify(); |
| assertEquals('Before action must not dispatch for pending undo.', |
| 1, beforeUndoCount); |
| |
| // Dispatching undo completed on first undo, should cause the second pending |
| // undo to happen. |
| mockState1.$reset(); |
| mockState1.undo(); |
| mockState1.$replay(); |
| mockState2.$replay(); // Nothing should happen to mockState2. |
| stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED); |
| mockState1.$verify(); |
| mockState2.$verify(); |
| assertEquals('Second undo must cause before action to dispatch', |
| 2, beforeUndoCount); |
| |
| // Test last undo. |
| mockState2.$reset(); |
| mockState2.undo(); |
| mockState2.$replay(); |
| manager.undo(); |
| mockState2.$verify(); |
| assertEquals('Third undo must cause before action to dispatch', |
| 3, beforeUndoCount); |
| } |
| |
| |
| function testRedo() { |
| addStatesToManager(); |
| manager.undo(); |
| manager.undo(); |
| manager.undo(); |
| resetStates(); |
| stateChangeCount = 0; |
| |
| mockState1.redo(); |
| mockState1.$replay(); |
| manager.redo(); |
| assertEquals('Pushing first item onto undo stack during redo must dispatch ' + |
| 'state change.', 1, stateChangeCount); |
| assertEquals('First redo must cause before action to dispatch', |
| 1, beforeRedoCount); |
| mockState1.$verify(); |
| |
| preventDefault = true; |
| mockState2.$replay(); |
| manager.redo(); |
| assertEquals('No stack transitions between 0 and 1, must not dispatch ' + |
| 'state change.', 1, stateChangeCount); |
| assertEquals('Second redo must cause before action to dispatch', |
| 2, beforeRedoCount); |
| mockState2.$verify(); // Verify that redo was prevented. |
| |
| preventDefault = false; |
| mockState3.redo(); |
| mockState3.$replay(); |
| manager.redo(); |
| assertEquals('Removing last item from redo stack must dispatch state change.', |
| 2, stateChangeCount); |
| assertEquals('Third redo must cause before action to dispatch', |
| 3, beforeRedoCount); |
| mockState3.$verify(); |
| mockState3.$reset(); |
| |
| mockState3.undo(); |
| mockState3.$replay(); |
| manager.undo(); |
| assertEquals('Putting item on redo stack must dispatch state change.', |
| 3, stateChangeCount); |
| assertEquals('Undo must cause before action to dispatch', |
| 4, beforeUndoCount); |
| mockState3.$verify(); |
| } |
| |
| |
| function testRedo_Asynchronous() { |
| var stubState = new goog.editor.plugins.UndoRedoState(true); |
| var redoCalled = false; |
| stubState.redo = function() { |
| redoCalled = true; |
| }; |
| stubState.undo = goog.nullFunction; |
| stubState.equals = function() { |
| return false; |
| }; |
| |
| manager.addState(stubState); |
| manager.addState(mockState1); |
| manager.addState(mockState2); |
| |
| manager.undo(); |
| manager.undo(); |
| manager.undo(); |
| stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED); |
| resetStates(); |
| |
| manager.redo(); |
| assertTrue('redoCalled must be true (redo must be called).', redoCalled); |
| |
| // Calling redo shouldn't actually redo since the first async redo hasn't |
| // fired an event yet. |
| mockState1.$replay(); |
| manager.redo(); |
| mockState1.$verify(); |
| |
| // Dispatching redo completed on first redo, should cause the second pending |
| // redo to happen. |
| mockState1.$reset(); |
| mockState1.redo(); |
| mockState1.$replay(); |
| mockState2.$replay(); // Nothing should happen to mockState1. |
| stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED); |
| mockState1.$verify(); |
| mockState2.$verify(); |
| |
| // Test last redo. |
| mockState2.$reset(); |
| mockState2.redo(); |
| mockState2.$replay(); |
| manager.redo(); |
| mockState2.$verify(); |
| } |
| |
| function testUndoAndRedoPeek() { |
| addStatesToManager(); |
| manager.undo(); |
| |
| assertEquals('redoPeek must return the top of the redo stack.', |
| manager.redoStack_[manager.redoStack_.length - 1], manager.redoPeek()); |
| assertEquals('undoPeek must return the top of the undo stack.', |
| manager.undoStack_[manager.undoStack_.length - 1], manager.undoPeek()); |
| } |