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