undoredomanager_test.js

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