UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

547 lines (453 loc) 23.8 kB
/** * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*global runtime, core, gui, ops*/ /** * @constructor * @param {core.UnitTestRunner} runner * @implements {core.UnitTest} */ gui.TrivialUndoManagerTests = function TrivialUndoManagerTests(runner) { "use strict"; var t, testarea, r = runner, member, cursor; cursor = member = function (id) { return { getMemberId: function () { return id; } }; }; /** * @param rootElement * @constructor * @implements {ops.Document} */ function AdaptiveMock(rootElement) { var self = this; /*jslint emptyblock: true*/ function noOp() { } function noOp2() { return true; } /*jslint emptyblock: false*/ function returnThis() { return self; } this.rootElement = rootElement; this.getCanvas = returnThis; this.odfContainer = returnThis; this.getAnnotationViewManager = returnThis; this.forgetAnnotations = noOp; this.refreshAnnotations = noOp; this.refreshCSS = noOp; this.setRootElement = noOp; this.setOdfContainer = noOp; this.cursors = [cursor("1")]; this.members = [member("1")]; this.getMemberIds = function () { return self.members.map(function (member) { return member.getMemberId(); }); }; this.hasCursor = function (memberid) { var i; for (i = 0; i < self.cursors.length; i += 1) { if (self.cursors[i].getMemberId() === memberid) { return true; } } return false; }; this.getDocumentElement = function () { return rootElement; }; this.getRootNode = function () { return rootElement; }; this.getDOMDocument = function () { return rootElement.ownerDocument; }; this.cloneDocumentElement = function () { return rootElement; }; this.setDocumentElement = noOp; this.removeCursor = noOp2; this.subscribe = noOp; this.unsubscribe = noOp; this.createRootFilter = function () { return new core.PositionFilterChain(); }; this.createPositionIterator = function() { throw new Error("Not implemented"); }; } /** * @param {!gui.TrivialUndoManager} undoManager * @constructor */ function DocumentModifiedChangeListener(undoManager) { var documentModified = null; /** * @param {!boolean} modified * @return {undefined} */ function onDocumentModifiedChanged(modified) { documentModified = modified; } /** * @return {?boolean} */ this.getDocumentModified = function() { return documentModified; }; // init undoManager.subscribe(gui.UndoManager.signalDocumentModifiedChanged, onDocumentModifiedChanged); } this.setUp = function () { testarea = core.UnitTest.provideTestAreaDiv(); t = { manager : new gui.TrivialUndoManager(), mock : new AdaptiveMock(testarea), ops : [] }; t.manager.setDocument(t.mock); t.manager.setPlaybackFunction(function (ops) { ops.forEach(function (op) { t.ops.push(op.spec().timestamp); }); }); }; this.tearDown = function () { t = {}; core.UnitTest.cleanupTestAreaDiv(); }; function create(operation, args) { args.memberid = args.memberid || 1; operation.init(args); return operation; } function hasUndoStates_OnlyMovesBackValidStates() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.moveBackward(1)", "0"); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.moveBackward(1)", "0"); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); r.shouldBe(t, "t.manager.moveBackward(1)", "1"); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.moveBackward(1)", "0"); } function hasRedoStates_OnlyMovesForwardValidStates() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); r.shouldBe(t, "t.manager.moveForward(1)", "0"); t.manager.moveBackward(1); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); r.shouldBe(t, "t.manager.moveForward(1)", "0"); // Create something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); t.manager.moveBackward(1); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); r.shouldBe(t, "t.manager.moveForward(1)", "1"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); r.shouldBe(t, "t.manager.moveForward(1)", "0"); } function setInitialState_SavesMostRecentCursorState() { t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 2})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); t.manager.setInitialState(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); // Now make something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); // And then undo it t.manager.moveBackward(1); // Should be back at origin r.shouldBe(t, "t.ops", "[3]"); } function setInitialState_AllCursorsMaintainedWhenCalledMultipleTimes() { t.mock.members.push(member(2)); t.mock.cursors.push(cursor(2)); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1, memberid: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 2, memberid: 2})); t.manager.setInitialState(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 4, memberid: 2})); t.manager.setInitialState(); t.manager.setInitialState(); t.manager.setInitialState(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); // Now make something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); // And then undo it t.manager.moveBackward(1); // Should be back at origin // Internally move operations are stored as object keys. This results in independent // move operations being ordered unpredictably across implementations (e.g., this failed in qtruntime) // As the timestamps are unique, and the order is irrelevant due to these being independent cursors, // this can just be sorted to ensure a deterministic order. t.ops.sort(); r.shouldBe(t, "t.ops", "[1,4]"); } function initialize_WhenNotInitialized_SavesInitialState() { t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 2})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); t.manager.initialize(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); // Now make something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); // And then undo it t.manager.moveBackward(1); // Should be back at origin r.shouldBe(t, "t.ops", "[3]"); } function initialize_WhenAlreadyInitialized_DoesNothing() { t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 2})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); t.manager.initialize(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); // Now make something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); // Now re-initialize and ensure nothing else happens t.manager.initialize(); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); // And then undo it t.manager.moveBackward(1); // Should be back at origin r.shouldBe(t, "t.ops", "[3]"); } function purgeInitialState_ClearsAllStacks() { t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 2})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); t.manager.setInitialState(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 6})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 7})); t.manager.moveBackward(1); // Get to the worst possible state ever r.shouldBe(t, "t.manager.hasUndoStates()", "true"); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); t.ops = []; // Now make something that can be undone // And now reset it t.manager.purgeInitialState(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); r.shouldBe(t, "t.manager.moveBackward(1)", "0"); r.shouldBe(t, "t.manager.moveForward(1)", "0"); t.manager.setInitialState(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 8})); t.manager.moveBackward(1); r.shouldBe(t, "t.ops", "[]"); } function moveBackward_MovesBack_InUndoQueue() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 10})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 20})); t.manager.moveBackward(1); r.shouldBe(t, "t.ops", "[5, 10]"); } function moveBackward_NextOperationEdit_ClearsRedo() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.moveBackward(1); // Now to 0 undo states available r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 1})); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); r.shouldBe(t, "t.manager.moveForward(1)", "0"); } function moveBackward_NextOperationNonEdit_LeavesRedoInTact() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.moveBackward(1); // Now to 0 undo states available r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); r.shouldBe(t, "t.manager.moveForward(1)", "1"); } function moveBackward_BoundaryCheck_InitialDocumentState() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.moveBackward(1); // Now to 0 undo states available r.shouldBe(t, "t.manager.hasUndoStates()", "false"); t.ops = []; // Want to ensure the OpMoveCursor is still applied if rewound back to initial state t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 6})); t.manager.moveBackward(1); // Now to 0 undo states available again r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.ops", "[1]"); } function moveBackward_ResetsMostRecentCursorState_ForVisibleCursors() { t.manager.onOperationExecuted(create(new ops.OpAddCursor(), {timestamp: 1, memberid: 1})); t.manager.onOperationExecuted(create(new ops.OpAddCursor(), {timestamp: 2, memberid: 2})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3, memberid: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 4, memberid: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 5, memberid: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 6, memberid: 2})); t.mock.members = [member(2)]; t.mock.cursors = [cursor(2)]; t.manager.initialize(); r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "false"); // Now make something that can be undone t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 8})); r.shouldBe(t, "t.manager.hasUndoStates()", "true"); // And then undo it t.manager.moveBackward(1); // Should be back at origin r.shouldBe(t, "t.ops", "[2, 6]"); } function undoState_ConsumesTrailingNonEditOps() { t.manager.initialize(); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); t.manager.moveBackward(1); // Now to 0 undo states available r.shouldBe(t, "t.manager.hasUndoStates()", "false"); r.shouldBe(t, "t.manager.hasRedoStates()", "true"); r.shouldBe(t, "t.ops", "[]"); } function documentModified_NonEditOp() { t.modifiedChangeListener = new DocumentModifiedChangeListener(t.manager); t.manager.initialize(); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); t.manager.onOperationExecuted(create(new ops.OpAddCursor(), {timestamp: 1, memberid: 1})); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); t.manager.moveBackward(1); // back to initial state which was false r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); } function documentModified_EditOp() { t.modifiedChangeListener = new DocumentModifiedChangeListener(t.manager); t.manager.initialize(); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 5})); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.moveBackward(1); // back to initial state which was false r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); } function setDocumentModified() { t.modifiedChangeListener = new DocumentModifiedChangeListener(t.manager); t.manager.initialize(); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); t.manager.setDocumentModified(true); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.setDocumentModified(false); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); // noedit op after setDocumentModified t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 1})); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); // edit op after setDocumentModified t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 3})); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.setDocumentModified(false); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); } function setDocumentModified_forwardBackwardToUnmodified() { t.modifiedChangeListener = new DocumentModifiedChangeListener(t.manager); t.manager.initialize(); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.setDocumentModified(false); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); t.manager.moveBackward(1); // back one state r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.moveForward(1); // for one state r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); } function setDocumentModified_branchingBeforeUnmodified() { t.modifiedChangeListener = new DocumentModifiedChangeListener(t.manager); t.manager.initialize(); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "null"); // create two states t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 1})); t.manager.onOperationExecuted(create(new ops.OpMoveCursor(), {timestamp: 3})); t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 4})); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); t.manager.setDocumentModified(false); r.shouldBe(t, "t.manager.isDocumentModified()", "false"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "false"); // back one state t.manager.moveBackward(1); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); // create another second state t.manager.onOperationExecuted(create(new ops.OpInsertText(), {timestamp: 6})); r.shouldBe(t, "t.manager.isDocumentModified()", "true"); r.shouldBe(t, "t.modifiedChangeListener.getDocumentModified()", "true"); } this.tests = function () { return r.name([ hasUndoStates_OnlyMovesBackValidStates, hasRedoStates_OnlyMovesForwardValidStates, setInitialState_SavesMostRecentCursorState, setInitialState_AllCursorsMaintainedWhenCalledMultipleTimes, initialize_WhenNotInitialized_SavesInitialState, initialize_WhenAlreadyInitialized_DoesNothing, purgeInitialState_ClearsAllStacks, moveBackward_MovesBack_InUndoQueue, moveBackward_NextOperationEdit_ClearsRedo, moveBackward_NextOperationNonEdit_LeavesRedoInTact, moveBackward_BoundaryCheck_InitialDocumentState, moveBackward_ResetsMostRecentCursorState_ForVisibleCursors, undoState_ConsumesTrailingNonEditOps, documentModified_NonEditOp, documentModified_EditOp, setDocumentModified, setDocumentModified_forwardBackwardToUnmodified, setDocumentModified_branchingBeforeUnmodified ]); }; this.asyncTests = function () { return [ ]; }; }; gui.TrivialUndoManagerTests.prototype.description = function () { "use strict"; return "Test the TrivialUndoManager class."; };