blob: 4ea8a28860bc15c364082e5908f4f60cd2b186f9 [file] [log] [blame]
// 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());
}