8
0
Просмотр исходного кода

Merge branch 'master' into ckeditor5-engine/t/643

Szymon Cofalik 9 лет назад
Родитель
Сommit
6074e94349

+ 3 - 4
packages/ckeditor5-undo/.travis.yml

@@ -12,7 +12,6 @@ node_js:
 cache:
   - node_modules
 before_install:
-  - export CHROME_BIN=chromium-browser
   - export DISPLAY=:99.0
   - sh -e /etc/init.d/xvfb start
 install:
@@ -20,7 +19,7 @@ install:
   - npm install @ckeditor/ckeditor5-dev-tests
   - npm install codeclimate-test-reporter
 script:
-  - node_modules/.bin/ckeditor5-dev-tests --coverage --ignore-duplicates
+  - node_modules/.bin/ckeditor5-dev-tests-travis
 after_success:
-  - sed -i.backup 's/build\/\.automated-tests\/ckeditor5\/[^\/]*/src/g' build/.automated-tests/coverage/lcov.info
-  - node_modules/.bin/codeclimate-test-reporter < .build/coverage/lcov.info
+  - node_modules/.bin/codeclimate-test-reporter < build/.automated-tests/coverage/lcov.info
+

+ 2 - 3
packages/ckeditor5-undo/README.md

@@ -4,9 +4,8 @@ CKEditor 5 Undo Manager
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-undo.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-undo)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-undo.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-undo)
 [![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-undo/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-undo/coverage)
-[![Code Climate](https://codeclimate.com/github/ckeditor/ckeditor5-undo/badges/gpa.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-undo)
-[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-undo/status.svg)](https://david-dm.org/ckeditor/ckeditor5-undo#info=dependencies)
-[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-undo/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-undo#info=devDependencies)
+[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-undo/status.svg)](https://david-dm.org/ckeditor/ckeditor5-undo)
+[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-undo/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-undo?type=dev)
 
 Undo manager for CKEditor 5. More information about the project can be found at the following URL: <https://github.com/ckeditor/ckeditor5-undo>.
 

+ 1 - 1
packages/ckeditor5-undo/package.json

@@ -1,6 +1,6 @@
 {
   "name": "ckeditor5-undo",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "description": "Undo manager for CKEditor 5.",
   "keywords": [],
   "dependencies": {

+ 14 - 33
packages/ckeditor5-undo/src/basecommand.js

@@ -3,14 +3,17 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module undo/basecommand
+ */
+
 import Command from '../core/command/command.js';
-import transform from '../engine/model/delta/transform.js';
 
 /**
- * Base class for undo feature commands: {@link undo.UndoCommand} and {@link undo.RedoCommand}.
+ * Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
  *
  * @protected
- * @memberOf undo
+ * @extends module:core/command/command~Command
  */
 export default class BaseCommand extends Command {
 	constructor( editor ) {
@@ -19,11 +22,11 @@ export default class BaseCommand extends Command {
 		/**
 		 * Stack of items stored by the command. These are pairs of:
 		 *
-		 * * {@link engine.model.Batch batch} saved by the command,
-		 * * {@link engine.model.Selection selection} state at the moment of saving the batch.
+		 * * {@link module:engine/model/batch~Batch batch} saved by the command,
+		 * * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch.
 		 *
 		 * @protected
-		 * @member {Array} undo.BaseCommand#_stack
+		 * @member {Array} #_stack
 		 */
 		this._stack = [];
 
@@ -31,7 +34,7 @@ export default class BaseCommand extends Command {
 		 * Stores all batches that were created by this command.
 		 *
 		 * @protected
-		 * @member {WeakSet.<engine.model.Batch>} undo.BaseCommand#_createdBatches
+		 * @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches
 		 */
 		this._createdBatches = new WeakSet();
 
@@ -40,10 +43,10 @@ export default class BaseCommand extends Command {
 	}
 
 	/**
-	 * Stores a batch in the command, together with the selection state of the {@link engine.model.Document document}
+	 * Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
 	 * created by the editor which this command is registered to.
 	 *
-	 * @param {engine.model.Batch} batch The batch to add.
+	 * @param {module:engine/model/batch~Batch} batch The batch to add.
 	 */
 	addBatch( batch ) {
 		const selection = {
@@ -71,10 +74,10 @@ export default class BaseCommand extends Command {
 	}
 
 	/**
-	 * Restores the {@link engine.model.Document#selection document selection} state after a batch was undone.
+	 * Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
 	 *
 	 * @protected
-	 * @param {Array.<engine.model.Range>} ranges Ranges to be restored.
+	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
 	 * @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
 	 */
 	_restoreSelection( ranges, isBackward, deltas ) {
@@ -108,28 +111,6 @@ export default class BaseCommand extends Command {
 	}
 }
 
-// Performs a transformation of delta set `setToTransform` by given delta set `setToTransformBy`.
-// If `setToTransform` deltas are more important than `setToTransformBy` deltas, `isStrong` should be true.
-export function transformDelta( setToTransform, setToTransformBy, isStrong ) {
-	let results = [];
-
-	for ( let toTransform of setToTransform ) {
-		let to = [ toTransform ];
-
-		for ( let t = 0; t < to.length; t++ ) {
-			for ( let transformBy of setToTransformBy ) {
-				let transformed = transform( to[ t ], transformBy, isStrong );
-				to.splice( t, 1, ...transformed );
-				t = t - 1 + transformed.length;
-			}
-		}
-
-		results = results.concat( to );
-	}
-
-	return results;
-}
-
 // Transforms given range `range` by deltas from `document` history, starting from a delta with given `baseVersion`.
 // Returns an array containing one or more ranges, which are result of the transformation.
 function transformSelectionRange( range, deltas ) {

+ 19 - 14
packages/ckeditor5-undo/src/redocommand.js

@@ -3,25 +3,30 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module undo/redocommand
+ */
+
 import BaseCommand from './basecommand.js';
-import { transformDelta as transformDelta } from './basecommand.js';
+import { transformDeltaSets } from '../engine/model/delta/transform.js';
 
 /**
- * The redo command stores {@link engine.model.Batch batches} that were used to undo a batch by {@link undo.UndoCommand UndoCommand}.
- * It is able to redo a previously undone batch by reversing the undoing batches created by `UndoCommand`. The reversed batch is
- * also transformed by batches from {@link engine.model.Document#history history} that happened after it and are not other redo batches.
+ * The redo command stores {@link module:engine/model/batch~Batch batches} that were used to undo a batch by
+ * {@link module:undo/undocommand~UndoCommand}. It is able to redo a previously undone batch by reversing the undoing
+ * batches created by `UndoCommand`. The reversed batch is also transformed by batches from
+ * {@link module:engine/model/document~Document#history history} that happened after it and are not other redo batches.
  *
- * The redo command also takes care of restoring the {@link engine.model.Document#selection document selection} to the state before
- * an undone batch was applied.
+ * The redo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}
+ * to the state before an undone batch was applied.
  *
- * @memberOf undo
- * @extends undo.BaseCommand
+ * @extends module:undo/basecommand~BaseCommand
  */
 export default class RedoCommand extends BaseCommand {
 	/**
-	 * Executes the command. This method reverts the last {@link engine.model.Batch batch} added to the command's stack, applies
-	 * the reverted and transformed version on the {@link engine.model.Document document} and removes the batch from the stack.
-	 * Then, it restores the {@link engine.model.Document#selection document selection}.
+	 * Executes the command. This method reverts the last {@link module:engine/model/batch~Batch batch} added to
+	 * the command's stack, applies the reverted and transformed version on the
+	 * {@link module:engine/model/document~Document document} and removes the batch from the stack.
+	 * Then, it restores the {@link module:engine/model/document~Document#selection document selection}.
 	 *
 	 * @protected
 	 */
@@ -51,10 +56,10 @@ export default class RedoCommand extends BaseCommand {
 
 	/**
 	 * Redoes a batch by reversing the batch that has undone it, transforming that batch and applying it. This is
-	 * a helper method for {@link undo.RedoCommand#_doExecute}.
+	 * a helper method for {@link #_doExecute}.
 	 *
 	 * @private
-	 * @param {engine.model.Batch} storedBatch The batch whose deltas will be reversed, transformed and applied.
+	 * @param {module:engine/model/batch~Batch} storedBatch The batch whose deltas will be reversed, transformed and applied.
 	 */
 	_redo( storedBatch ) {
 		const document = this.editor.document;
@@ -83,7 +88,7 @@ export default class RedoCommand extends BaseCommand {
 			// again will result in incorrect deltas.
 			for ( let historyDelta of document.history.getDeltas( nextBaseVersion ) ) {
 				if ( !this._createdBatches.has( historyDelta.batch ) ) {
-					reversedDelta = transformDelta( reversedDelta, [ historyDelta ], true );
+					reversedDelta = transformDeltaSets( reversedDelta, [ historyDelta ], true ).deltasA;
 				}
 			}
 

+ 17 - 12
packages/ckeditor5-undo/src/undo.js

@@ -3,6 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module undo/undo
+ */
+
 import Plugin from '../core/plugin.js';
 import UndoEngine from './undoengine.js';
 import ButtonView from '../ui/button/buttonview.js';
@@ -10,12 +14,14 @@ import ButtonView from '../ui/button/buttonview.js';
 /**
  * The undo feature. It introduces the Undo and Redo buttons to the editor.
  *
- * Below is the explanation of the undo mechanism working together with {@link engine.model.History History}:
+ * Below is the explanation of the undo mechanism working together with {@link module:engine/model/history~History History}:
  *
- * Whenever a {@link engine.model.Delta delta} is applied to the {@link engine.model.Document document}, it is saved to
- * `History` as is. The {@link engine.model.Batch batch} that owns that delta is also saved, in {@link undo.UndoCommand},
- * together with the selection that was present in the document before the delta was applied. A batch is saved instead of the delta
- * because changes are undone batch-by-batch, not delta-by-delta and a batch is seen as one undo step.
+ * Whenever a {@link module:engine/model/delta/delta~Delta delta} is applied to the
+ * {@link module:engine/model/document~Document document}, it is saved to `History` as is.
+ * The {@link module:engine/model/batch~Batch batch} that owns that delta is also saved, in
+ * {@link module:undo/undocommand~UndoCommand}, together with the selection that was present in the document before the
+ * delta was applied. A batch is saved instead of the delta because changes are undone batch-by-batch, not delta-by-delta
+ * and a batch is seen as one undo step.
  *
  * After some changes happen to the document, the `History` and `UndoCommand` stack can be represented as follows:
  *
@@ -36,9 +42,9 @@ import ButtonView from '../ui/button/buttonview.js';
  * so if a batch has delta `X`, `Y`, `Z`, reversed deltas `Zr`, `Yr` and `Xr` need to be applied. Otherwise reversed delta
  * `Xr` would operate on a wrong document state, because delta `X` does not know that deltas `Y` and `Z` happened.
  *
- * After deltas from an undone batch got {@link engine.model.Delta#getReversed reversed}, one needs to make sure if they are
- * ready to be applied. In the scenario above, delta `C3` is the last delta and `C3r` bases on up-to-date document state, so
- * it can be applied to the document.
+ * After deltas from an undone batch got {@link module:engine/model/delta/delta~Delta#getReversed reversed},
+ * one needs to make sure if they are ready to be applied. In the scenario above, delta `C3` is the last delta and `C3r`
+ * bases on up-to-date document state, so it can be applied to the document.
  *
  *		  History                           Undo stack
  *		===========             ==================================
@@ -58,7 +64,7 @@ import ButtonView from '../ui/button/buttonview.js';
  * operational transformation algorithms assume there is no connection between two transformed operations when resolving
  * conflicts, which is true for example for collaborative editing, but is not true for the undo algorithm.
  *
- * To prevent both problems, `History` introduces an API to {@link engine.model.History#removeDelta remove}
+ * To prevent both problems, `History` introduces an API to {@link module:engine/model/history~History#removeDelta remove}
  * deltas from history. It is used to remove undone and undoing deltas after they are applied. It feels right &mdash; since when a
  * delta is undone or reversed, it is "removed" and there should be no sign of it in the history (fig. 1).
  *
@@ -92,7 +98,7 @@ import ButtonView from '../ui/button/buttonview.js';
  * It is an obvious error &mdash; transforming by that delta would lead to incorrect results or "repeating" history would
  * produce a different document than the actual one.
  *
- * To prevent this situation, `B3` needs to also be {@link engine.model.History#updateDelta updated} in history.
+ * To prevent this situation, `B3` needs to also be {@link module:engine/model/history~History#updateDelta updated} in history.
  * It should be kept in a state that "does not remember" deltas that were removed from history. It is easily
  * achieved while transforming the reversed delta. For example, when `C2r` is transformed by `B3`, at the same time `B3` is
  * transformed by `C2r`. Transforming `B3` that remembers `C2` by a delta reversing `C2` effectively makes `B3` "forget" about `C2`.
@@ -116,8 +122,7 @@ import ButtonView from '../ui/button/buttonview.js';
  * The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history,
  * simultaneously updating them. Then deltas are applied to the document and removed from history (fig. 5).
  *
- * @memberOf undo
- * @extends core.Plugin
+ * @extends module:core/plugin~Plugin
  */
 export default class Undo extends Plugin {
 	/**

+ 32 - 22
packages/ckeditor5-undo/src/undocommand.js

@@ -3,29 +3,33 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module undo/undocommand
+ */
+
 import BaseCommand from './basecommand.js';
-import { transformDelta, transformRangesByDeltas } from './basecommand.js';
+import { transformRangesByDeltas } from './basecommand.js';
+import { transformDeltaSets } from '../engine/model/delta/transform.js';
 
 /**
- * The undo command stores {@link engine.model.Batch batches} applied to the {@link engine.model.Document document}
- * ble to undo a batch by reversing it and transforming by other batches from {@link engine.model.Document#history history}
- * that happened after the reversed batch.
+ * The undo command stores {@link module:engine/model/batch~Batch batches} applied to the
+ * {@link module:engine/model/document~Document document} ble to undo a batch by reversing it and transforming by
+ * other batches from {@link module:engine/model/document~Document#history history} that happened after the reversed batch.
  *
- * The undo command also takes care of restoring the {@link engine.model.Document#selection document selection} to the state before
- * the undone batch was applied.
+ * The undo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}
+ * to the state before the undone batch was applied.
  *
- * @memberOf undo
- * @extends undo.BaseCommand
+ * @extends module:undo/basecommand~BaseCommand
  */
 export default class UndoCommand extends BaseCommand {
 	/**
-	 * Executes the command. This method reverts a {@link engine.model.Batch batch} added to the command's stack, transforms
-	 * and applies the reverted version on the {@link engine.model.Document document} and removes the batch from the stack.
-	 * Then, it restores the {@link engine.model.Document#selection document selection}.
+	 * Executes the command. This method reverts a {@link module:engine/model/batch~Batch batch} added to the command's stack, transforms
+	 * and applies the reverted version on the {@link module:engine/model/document~Document document} and removes the batch from the stack.
+	 * Then, it restores the {@link module:engine/model/document~Document#selection document selection}.
 	 *
 	 * @protected
-	 * @fires undo.UndoCommand#event:revert
-	 * @param {engine.model.Batch} [batch] A batch that should be undone. If not set, the last added batch will be undone.
+	 * @fires revert
+	 * @param {module:engine/model/batch~Batch} [batch] A batch that should be undone. If not set, the last added batch will be undone.
 	 */
 	_doExecute( batch = null ) {
 		// If batch is not given, set `batchIndex` to the last index in command stack.
@@ -48,8 +52,8 @@ export default class UndoCommand extends BaseCommand {
 	}
 
 	/**
-	 * Returns an index in {@link undo.BaseCommand#_stack} pointing to the item that is storing a batch that has a given
-	 * {@link engine.model.Batch#baseVersion}.
+	 * Returns an index in {@link module:undo/basecommand~BaseCommand#_stack} pointing to the item that is storing a
+	 * batch that has a given {@link module:engine/model/batch~Batch#baseVersion}.
 	 *
 	 * @private
 	 * @param {Number} baseVersion The base version of the batch to find.
@@ -67,10 +71,10 @@ export default class UndoCommand extends BaseCommand {
 
 	/**
 	 * Undoes a batch by reversing a batch from history, transforming that reversed batch and applying it. This is
-	 * a helper method for {@link undo.UndoCommand#_doExecute}.
+	 * a helper method for {@link #_doExecute}.
 	 *
 	 * @private
-	 * @param {engine.model.Batch} batchToUndo A batch whose deltas will be reversed, transformed and applied.
+	 * @param {module:engine/model/batch~Batch} batchToUndo A batch whose deltas will be reversed, transformed and applied.
 	 */
 	_undo( batchToUndo ) {
 		const document = this.editor.document;
@@ -130,13 +134,13 @@ export default class UndoCommand extends BaseCommand {
 					this._stack[ itemIndex ].selection.ranges = transformRangesByDeltas( this._stack[ itemIndex ].selection.ranges, reversedDelta );
 				}
 
-				// 3.2. Transform history delta by reversed delta. We need this to update document history.
-				const updatedHistoryDelta = transformDelta( [ historyDelta ], reversedDelta, false );
+				// 3.2. Transform reversed delta by history delta and vice-versa.
+				const results = transformDeltaSets( reversedDelta, [ historyDelta ], true );
 
-				// 3.3. Transform reversed delta by history delta (in state before transformation above).
-				reversedDelta = transformDelta( reversedDelta, [ historyDelta ], true );
+				reversedDelta = results.deltasA;
+				const updatedHistoryDelta = results.deltasB;
 
-				// 3.4. Store updated history delta. Later, it will be updated in `history`.
+				// 3.3. Store updated history delta. Later, it will be updated in `history`.
 				if ( !updatedHistoryDeltas[ historyDelta.baseVersion ] ) {
 					updatedHistoryDeltas[ historyDelta.baseVersion ] = [];
 				}
@@ -177,3 +181,9 @@ export default class UndoCommand extends BaseCommand {
 		return undoingBatch;
 	}
 }
+
+/**
+ * Fired when execution of the command reverts some batch.
+ *
+ * @event revert
+ */

+ 13 - 10
packages/ckeditor5-undo/src/undoengine.js

@@ -3,6 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+/**
+ * @module undo/undoengine
+ */
+
 import Plugin from '../core/plugin.js';
 import UndoCommand from './undocommand.js';
 import RedoCommand from './redocommand.js';
@@ -11,10 +15,9 @@ import RedoCommand from './redocommand.js';
  * The undo engine feature.
  *
  * Undo brings in possibility to undo and redo changes done in the model by deltas through
- * the {@link engine.model.Document#batch Batch API}.
+ * the {@link module:engine/model/document~Document#batch Batch API}.
  *
- * @memberOf undo
- * @extends core.Plugin
+ * @extends module:core/plugin~Plugin
  */
 export default class UndoEngine extends Plugin {
 	/**
@@ -24,26 +27,26 @@ export default class UndoEngine extends Plugin {
 		super( editor );
 
 		/**
-		 * The command that manages undo {@link engine.model.Batch batches} stack (history).
-		 * Created and registered during the {@link undo.UndoEngine#init feature initialization}.
+		 * The command that manages undo {@link module:engine/model/batch~Batch batches} stack (history).
+		 * Created and registered during the {@link #init feature initialization}.
 		 *
 		 * @private
-		 * @member {undo.UndoEngineCommand} undo.UndoEngine#_undoCommand
+		 * @member {undo.UndoEngineCommand} #_undoCommand
 		 */
 
 		/**
-		 * The command that manages redo {@link engine.model.Batch batches} stack (history).
-		 * Created and registered during the {@link undo.UndoEngine#init feature initialization}.
+		 * The command that manages redo {@link module:engine/model/batch~Batch batches} stack (history).
+		 * Created and registered during the {@link #init feature initialization}.
 		 *
 		 * @private
-		 * @member {undo.UndoEngineCommand} undo.UndoEngine#_redoCommand
+		 * @member {undo.UndoEngineCommand} #_redoCommand
 		 */
 
 		/**
 		 * Keeps track of which batches were registered in undo.
 		 *
 		 * @private
-		 * @member {WeakSet.<engine.model.Batch>} undo.UndoEngine#_batchRegistry
+		 * @member {WeakSet.<module:engine/model/batch~Batch>}
 		 */
 		this._batchRegistry = new WeakSet();
 	}

+ 11 - 11
packages/ckeditor5-undo/tests/basecommand.js

@@ -6,22 +6,22 @@
 import ModelTestEditor from 'tests/core/_utils/modeltesteditor.js';
 import BaseCommand from 'ckeditor5/undo/basecommand.js';
 
-let editor, doc, root, base;
+describe( 'BaseCommand', () => {
+	let editor, doc, root, base;
 
-beforeEach( () => {
-	editor = new ModelTestEditor();
-	base = new BaseCommand( editor );
+	beforeEach( () => {
+		editor = new ModelTestEditor();
+		base = new BaseCommand( editor );
 
-	doc = editor.document;
+		doc = editor.document;
 
-	root = doc.getRoot();
-} );
+		root = doc.getRoot();
+	} );
 
-afterEach( () => {
-	base.destroy();
-} );
+	afterEach( () => {
+		base.destroy();
+	} );
 
-describe( 'BaseCommand', () => {
 	describe( 'constructor()', () => {
 		it( 'should create command with empty batch stack', () => {
 			expect( base._checkEnabled() ).to.be.false;

+ 0 - 2
packages/ckeditor5-undo/tests/manual/undo.md

@@ -1,3 +1 @@
-@bender-ui: collapsed
-
 Test the undo/redo features.

+ 222 - 220
packages/ckeditor5-undo/tests/redocommand.js

@@ -10,244 +10,246 @@ import UndoCommand from 'ckeditor5/undo/undocommand.js';
 import RedoCommand from 'ckeditor5/undo/redocommand.js';
 import { itemAt, getText } from 'tests/engine/model/_utils/utils.js';
 
-let editor, doc, root, redo, undo;
+describe( 'RedoCommand', () => {
+	let editor, doc, root, redo, undo;
 
-beforeEach( () => {
-	editor = new ModelTestEditor();
-	redo = new RedoCommand( editor );
+	beforeEach( () => {
+		editor = new ModelTestEditor();
+		redo = new RedoCommand( editor );
 
-	doc = editor.document;
+		doc = editor.document;
 
-	root = doc.getRoot();
-} );
+		root = doc.getRoot();
+	} );
 
-afterEach( () => {
-	redo.destroy();
-} );
+	afterEach( () => {
+		redo.destroy();
+	} );
 
-describe( 'RedoCommand', () => {
-	describe( '_execute', () => {
-		const p = pos => new Position( root, [].concat( pos ) );
-		const r = ( a, b ) => new Range( p( a ), p( b ) );
-
-		let batch0, batch1, batch2;
-		let batches = new Set();
-
-		beforeEach( () => {
-			undo = new UndoCommand( editor );
-
-			// Simple integration with undo.
-			undo.on( 'revert', ( evt, undoneBatch, undoingBatch ) => {
-				if ( !batches.has( undoingBatch ) ) {
-					redo.addBatch( undoingBatch );
-					batches.add( undoingBatch );
-				}
+	describe( 'RedoCommand', () => {
+		describe( '_execute', () => {
+			const p = pos => new Position( root, [].concat( pos ) );
+			const r = ( a, b ) => new Range( p( a ), p( b ) );
+
+			let batch0, batch1, batch2;
+			let batches = new Set();
+
+			beforeEach( () => {
+				undo = new UndoCommand( editor );
+
+				// Simple integration with undo.
+				undo.on( 'revert', ( evt, undoneBatch, undoingBatch ) => {
+					if ( !batches.has( undoingBatch ) ) {
+						redo.addBatch( undoingBatch );
+						batches.add( undoingBatch );
+					}
+				} );
+
+				/*
+				 [root]
+				 - {}
+				 */
+				editor.document.selection.setRanges( [ r( 0, 0 ) ] );
+				batch0 = doc.batch();
+				undo.addBatch( batch0 );
+				batch0.insert( p( 0 ), 'foobar' );
+				/*
+				 [root]
+				 - f
+				 - o
+				 - o
+				 - b
+				 - a
+				 - r{}
+				 */
+				// Let's make things spicy and this time, make a backward selection.
+				editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
+				batch1 = doc.batch();
+				undo.addBatch( batch1 );
+				batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
+				/*
+				 [root]
+				 - f
+				 - o
+				 - {o (key: value)
+				 - b} (key: value)
+				 - a
+				 - r
+				 */
+				editor.document.selection.setRanges( [ r( 1, 3 ) ] );
+				batch2 = doc.batch();
+				undo.addBatch( batch2 );
+				batch2.move( r( 1, 3 ), p( 6 ) );
+				/*
+				 [root]
+				 - f
+				 - b (key: value)
+				 - a
+				 - r
+				 - {o
+				 - o} (key: value)
+				 */
 			} );
 
-			/*
-			 [root]
-			 - {}
-			 */
-			editor.document.selection.setRanges( [ r( 0, 0 ) ] );
-			batch0 = doc.batch();
-			undo.addBatch( batch0 );
-			batch0.insert( p( 0 ), 'foobar' );
-			/*
-			 [root]
-			 - f
-			 - o
-			 - o
-			 - b
-			 - a
-			 - r{}
-			 */
-			// Let's make things spicy and this time, make a backward selection.
-			editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
-			batch1 = doc.batch();
-			undo.addBatch( batch1 );
-			batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
-			/*
-			 [root]
-			 - f
-			 - o
-			 - {o (key: value)
-			 - b} (key: value)
-			 - a
-			 - r
-			 */
-			editor.document.selection.setRanges( [ r( 1, 3 ) ] );
-			batch2 = doc.batch();
-			undo.addBatch( batch2 );
-			batch2.move( r( 1, 3 ), p( 6 ) );
-			/*
-			 [root]
-			 - f
-			 - b (key: value)
-			 - a
-			 - r
-			 - {o
-			 - o} (key: value)
-			 */
-		} );
-
-		it( 'should redo batch undone by undo command', () => {
-			undo._execute( batch2 );
-
-			redo._execute();
-			// Should be back at original state:
-			/*
-			 [root]
-			 - f
-			 - b (key: value)
-			 - a
-			 - r
-			 - {o
-			 - o} (key: value)
-			 */
-			expect( getText( root ) ).to.equal( 'fbaroo' );
-			expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-		} );
+			it( 'should redo batch undone by undo command', () => {
+				undo._execute( batch2 );
+
+				redo._execute();
+				// Should be back at original state:
+				/*
+				 [root]
+				 - f
+				 - b (key: value)
+				 - a
+				 - r
+				 - {o
+				 - o} (key: value)
+				 */
+				expect( getText( root ) ).to.equal( 'fbaroo' );
+				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+			} );
 
-		it( 'should redo series of batches undone by undo command', () => {
-			undo._execute();
-			undo._execute();
-			undo._execute();
-
-			redo._execute();
-			// Should be like after applying `batch0`:
-			/*
-			 [root]
-			 - f
-			 - o
-			 - o
-			 - b
-			 - a
-			 - r{}
-			 */
-			expect( getText( root ) ).to.equal( 'foobar' );
-			expect( Array.from( root.getChildren() ).find( node => node.hasAttribute( 'key' ) ) ).to.be.undefined;
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-
-			redo._execute();
-			// Should be like after applying `batch1`:
-			/*
-			 [root]
-			 - f
-			 - o
-			 - {o (key: value)
-			 - b} (key: value)
-			 - a
-			 - r
-			 */
-			expect( getText( root ) ).to.equal( 'foobar' );
-			expect( itemAt( root, 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 2, 4 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.true;
-
-			redo._execute();
-			// Should be like after applying `batch2`:
-			/*
-			 [root]
-			 - f
-			 - b (key: value)
-			 - a
-			 - r
-			 - {o
-			 - o} (key: value)
-			 */
-			expect( getText( root ) ).to.equal( 'fbaroo' );
-			expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-		} );
+			it( 'should redo series of batches undone by undo command', () => {
+				undo._execute();
+				undo._execute();
+				undo._execute();
+
+				redo._execute();
+				// Should be like after applying `batch0`:
+				/*
+				 [root]
+				 - f
+				 - o
+				 - o
+				 - b
+				 - a
+				 - r{}
+				 */
+				expect( getText( root ) ).to.equal( 'foobar' );
+				expect( Array.from( root.getChildren() ).find( node => node.hasAttribute( 'key' ) ) ).to.be.undefined;
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+
+				redo._execute();
+				// Should be like after applying `batch1`:
+				/*
+				 [root]
+				 - f
+				 - o
+				 - {o (key: value)
+				 - b} (key: value)
+				 - a
+				 - r
+				 */
+				expect( getText( root ) ).to.equal( 'foobar' );
+				expect( itemAt( root, 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 2, 4 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.true;
+
+				redo._execute();
+				// Should be like after applying `batch2`:
+				/*
+				 [root]
+				 - f
+				 - b (key: value)
+				 - a
+				 - r
+				 - {o
+				 - o} (key: value)
+				 */
+				expect( getText( root ) ).to.equal( 'fbaroo' );
+				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+			} );
 
-		it( 'should redo batch selectively undone by undo command', () => {
-			undo._execute( batch0 );
-			redo._execute();
-
-			// Should be back to original state:
-			/*
-			 [root]
-			 - f
-			 - b (key: value)
-			 - a
-			 - r
-			 - o
-			 - o{} (key: value)
-			 */
-			expect( getText( root ) ).to.equal( 'fbaroo' );
-			expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-		} );
+			it( 'should redo batch selectively undone by undo command', () => {
+				undo._execute( batch0 );
+				redo._execute();
+
+				// Should be back to original state:
+				/*
+				 [root]
+				 - f
+				 - b (key: value)
+				 - a
+				 - r
+				 - o
+				 - o{} (key: value)
+				 */
+				expect( getText( root ) ).to.equal( 'fbaroo' );
+				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+			} );
 
-		it( 'should redo batch selectively undone by undo command #2', () => {
-			undo._execute( batch1 );
-			undo._execute( batch2 );
-			redo._execute();
-			redo._execute();
-
-			// Should be back to original state:
-			/*
-			 [root]
-			 - f
-			 - {b} (key: value)
-			 - a
-			 - r
-			 - o
-			 - o (key: value)
-			 */
-			expect( getText( root ) ).to.equal( 'fbaroo' );
-			expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getRanges().next().value.isEqual( r( 1, 2 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.true;
-		} );
+			it( 'should redo batch selectively undone by undo command #2', () => {
+				undo._execute( batch1 );
+				undo._execute( batch2 );
+				redo._execute();
+				redo._execute();
+
+				// Should be back to original state:
+				/*
+				 [root]
+				 - f
+				 - {b} (key: value)
+				 - a
+				 - r
+				 - o
+				 - o (key: value)
+				 */
+				expect( getText( root ) ).to.equal( 'fbaroo' );
+				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getRanges().next().value.isEqual( r( 1, 2 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.true;
+			} );
 
-		it( 'should transform redo batch by changes written in history that happened after undo but before redo #2', () => {
-			// Now it is "fBaroO".
-			// Undo moving "oo" to the end of string. Now it is "foOBar". Capitals mean set attribute.
-			undo._execute();
+			it( 'should transform redo batch by changes written in history that happened after undo but before redo #2', () => {
+				// Now it is "fBaroO".
+				// Undo moving "oo" to the end of string. Now it is "foOBar". Capitals mean set attribute.
+				undo._execute();
 
-			// Remove "ar".
-			doc.batch().remove( r( 4, 6 ) );
+				// Remove "ar".
+				doc.batch().remove( r( 4, 6 ) );
 
-			// Undo setting attribute on "ob". Now it is "foob".
-			undo._execute();
+				// Undo setting attribute on "ob". Now it is "foob".
+				undo._execute();
 
-			// Append "xx" at the beginning. Now it is "xxfoob".
-			doc.batch().insert( p( 0 ), 'xx' );
+				// Append "xx" at the beginning. Now it is "xxfoob".
+				doc.batch().insert( p( 0 ), 'xx' );
 
-			// Redo setting attribute on "ob". Now it is "xxfoOB".
-			redo._execute();
+				// Redo setting attribute on "ob". Now it is "xxfoOB".
+				redo._execute();
 
-			expect( getText( root ) ).to.equal( 'xxfoob' );
-			expect( itemAt( root, 4 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.true;
+				expect( getText( root ) ).to.equal( 'xxfoob' );
+				expect( itemAt( root, 4 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.true;
 
-			// Redo moving "oo". Now it is "xxfBoO". Selection is expected to be on just moved "oO".
-			redo._execute();
+				// Redo moving "oo". Now it is "xxfBoO". Selection is expected to be on just moved "oO".
+				redo._execute();
 
-			expect( getText( root ) ).to.equal( 'xxfboo' );
-			expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
+				expect( getText( root ) ).to.equal( 'xxfboo' );
+				expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+			} );
 		} );
 	} );
 } );

+ 340 - 338
packages/ckeditor5-undo/tests/undocommand.js

@@ -11,356 +11,358 @@ import UndoCommand from 'ckeditor5/undo/undocommand.js';
 import AttributeDelta from 'ckeditor5/engine/model/delta/attributedelta.js';
 import { itemAt, getText } from 'tests/engine/model/_utils/utils.js';
 
-let editor, doc, root, undo;
-
-beforeEach( () => {
-	editor = new ModelTestEditor();
-	undo = new UndoCommand( editor );
-
-	doc = editor.document;
-
-	root = doc.getRoot();
-} );
-
-afterEach( () => {
-	undo.destroy();
-} );
-
 describe( 'UndoCommand', () => {
-	const p = pos => new Position( root, [].concat( pos ) );
-	const r = ( a, b ) => new Range( p( a ), p( b ) );
-
-	describe( '_execute', () => {
-		let batch0, batch1, batch2, batch3;
-
-		beforeEach( () => {
-			/*
-			 [root]
-			 - {}
-			 */
-			editor.document.selection.setRanges( [ r( 0, 0 ) ] );
-			batch0 = doc.batch();
-			undo.addBatch( batch0 );
-			batch0.insert( p( 0 ), 'foobar' );
-			/*
-			 [root]
-			 - f
-			 - o
-			 - o
-			 - b
-			 - a
-			 - r{}
-			 */
-			// Let's make things spicy and this time, make a backward selection.
-			editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
-			batch1 = doc.batch();
-			undo.addBatch( batch1 );
-			batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
-			/*
-			 [root]
-			 - f
-			 - o
-			 - {o (key: value)
-			 - b} (key: value)
-			 - a
-			 - r
-			 */
-			editor.document.selection.setRanges( [ r( 1, 3 ) ] );
-			batch2 = doc.batch();
-			undo.addBatch( batch2 );
-			batch2.move( r( 1, 3 ), p( 6 ) );
-			/*
-			 [root]
-			 - f
-			 - b (key: value)
-			 - a
-			 - r
-			 - {o
-			 - o} (key: value)
-			 */
-			editor.document.selection.setRanges( [ r( 1, 4 ) ] );
-			batch3 = doc.batch();
-			undo.addBatch( batch3 );
-			batch3.wrap( r( 1, 4 ), 'p' );
-			/*
-			 [root]
-			 - f
-			 - [p]
-			 	- {b (key: value)
-			 	- a
-			 	- r}
-			 - o
-			 - o (key: value)
-			 */
-			editor.document.selection.setRanges( [ r( 0, 1 ) ] );
-			batch2.move( r( 0, 1 ), p( 3 ) );
-			/*
-			 [root]
-			 - [p]
-			 	- b (key: value)
-			 	- a
-			 	- r
-			 - o
-			 - f
-			 - o{} (key: value)
-			 */
-			editor.document.selection.setRanges( [ r( 4, 4 ) ] );
-		} );
-
-		it( 'should revert changes done by deltas from the batch that was most recently added to the command stack', () => {
-			undo._execute();
-
-			// Selection is restored. Wrap is removed:
-			/*
-			 [root]
-			 - {b (key: value)
-			 - a
-			 - r}
-			 - o
-			 - f
-			 - o (key: value)
-			 */
-
-			expect( getText( root ) ).to.equal( 'barofo' );
-			expect( itemAt( root, 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-
-			undo._execute();
-
-			// Two moves are removed:
-			/*
-			 [root]
-			 - f
-			 - {o
-			 - o} (key: value)
-			 - b (key: value)
-			 - a
-			 - r
-			 */
-
-			expect( getText( root ) ).to.equal( 'foobar' );
-			expect( itemAt( root, 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
-
-			expect( editor.document.selection.getFirstRange().isEqual( r( 1, 3 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-
-			undo._execute();
-
-			// Set attribute is undone:
-			/*
-			 [root]
-			 - f
-			 - o
-			 - {o
-			 - b}
-			 - a
-			 - r
-			 */
-
-			expect( getText( root ) ).to.equal( 'foobar' );
-			expect( itemAt( root, 2 ).hasAttribute( 'key' ) ).to.be.false;
-			expect( itemAt( root, 3 ).hasAttribute( 'key' ) ).to.be.false;
-
-			expect( editor.document.selection.getFirstRange().isEqual( r( 2, 4 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.true;
-
-			undo._execute();
-
-			// Insert is undone:
-			/*
-			 [root]
-			 */
-
-			expect( root.childCount ).to.equal( 0 );
-			expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
-		} );
-
-		it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert set attribute)', () => {
-			undo._execute( batch1 );
-			// Remove attribute:
-			/*
-			 [root]
-			 - [p]
-			 	- b
-			 	- a
-			 	- r
-			 - o
-			 - f
-			 - o
-			 */
-
-			expect( itemAt( root, 0 ).name ).to.equal( 'p' );
-			expect( getText( root.getChild( 0 ) ) ).to.equal( 'bar' );
-			expect( root.getChild( 1 ).data ).to.equal( 'ofo' );
-
-			expect( itemAt( root.getChild( 0 ), 0 ).hasAttribute( 'key' ) ).to.be.false;
-			expect( itemAt( root, 2 ).hasAttribute( 'key' ) ).to.be.false;
-			expect( itemAt( root, 3 ).hasAttribute( 'key' ) ).to.be.false;
-
-			// Selection is only partially restored because the range got broken.
-			// The selection would have to best on letter "b" and letter "o", but it is set only on letter "b".
-			expect( editor.document.selection.getFirstRange().isEqual( r( [ 0, 0 ], [ 0, 1 ] ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.true;
-		} );
-
-		it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert insert foobar)', () => {
-			undo._execute( batch0 );
-			// Remove foobar:
-			/*
-			 [root]
-			 - [p]
-			 */
-
-			// The `P` element wasn't removed because it wasn`t added by undone batch.
-			// It would be perfect if the `P` got removed aswell because wrapping was on removed nodes.
-			// But this would need a lot of logic / hardcoded ifs or a post-fixer.
-			expect( root.childCount ).to.equal( 1 );
-			expect( itemAt( root, 0 ).name ).to.equal( 'p' );
-
-			expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-
-			undo._execute( batch1 );
-			// Remove attributes.
-			// This does nothing in the `root` because attributes were set on nodes that already got removed.
-			// But those nodes should change in the graveyard and we can check them there.
+	let editor, doc, root, undo;
 
-			expect( root.childCount ).to.equal( 1 );
-			expect( itemAt( root, 0 ).name ).to.equal( 'p' );
+	beforeEach( () => {
+		editor = new ModelTestEditor();
+		undo = new UndoCommand( editor );
 
-			// Operations for undoing that batch were working on graveyard so document selection should not change.
-			expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
+		doc = editor.document;
 
-			expect( doc.graveyard.getChild( 0 ).maxOffset ).to.equal( 6 );
-
-			for ( let char of doc.graveyard._children ) {
-				expect( char.hasAttribute( 'key' ) ).to.be.false;
-			}
-
-			// Let's undo wrapping. This should leave us with empty root.
-			undo._execute( batch3 );
-			expect( root.maxOffset ).to.equal( 0 );
-
-			// Once again transformed range ends up in the graveyard.
-			expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
-			expect( editor.document.selection.isBackward ).to.be.false;
-		} );
+		root = doc.getRoot();
 	} );
 
-	// Some tests to ensure 100% CC and proper behavior in edge cases.
-	describe( 'edge cases', () => {
-		function getCaseText( root ) {
-			let text = '';
-
-			for ( let i = 0; i < root.childCount; i++ ) {
-				let node = root.getChild( i );
-				text += node.getAttribute( 'uppercase' ) ? node.data.toUpperCase() : node.data;
-			}
-
-			return text;
-		}
-
-		it( 'correctly handles deltas in compressed history that were earlier updated into multiple deltas (or split when undoing)', () => {
-			// In this case we assume that one of the deltas in compressed history was updated to two deltas.
-			// This is a tricky edge case because it is almost impossible to come up with convincing scenario that produces it.
-			// At the moment of writing this test and comment, only Undo feature uses `CompressedHistory#updateDelta`.
-			// Because deltas that "stays" in history are transformed with `isStrong` flag set to `false`, `MoveOperation`
-			// won't get split and `AttributeDelta` can hold multiple `AttributeOperation` in it. So using most common deltas
-			// (`InsertDelta`, `RemoveDelta`, `MoveDelta`, `AttributeDelta`) and undo it's impossible to get to this edge case.
-			// Still there might be some weird scenarios connected with OT / Undo / Collaborative Editing / other deltas /
-			// fancy 3rd party plugin where it may come up, so it's better to be safe than sorry.
-
-			root.appendChildren( new Text( 'abcdef' ) );
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
-
-			editor.document.selection.setRanges( [ r( 1, 4 ) ] );
-			let batch0 = doc.batch();
-			undo.addBatch( batch0 );
-			batch0.move( r( 1, 4 ), p( 5 ) );
-			expect( getCaseText( root ) ).to.equal( 'aebcdf' );
-
-			editor.document.selection.setRanges( [ r( 1, 1 ) ]  );
-			let batch1 = doc.batch();
-			undo.addBatch( batch1 );
-			batch1.remove( r( 0, 1 ) );
-			expect( getCaseText( root ) ).to.equal( 'ebcdf' );
-
-			editor.document.selection.setRanges( [ r( 0, 3 ) ] );
-			let batch2 = doc.batch();
-			undo.addBatch( batch2 );
-			batch2.setAttribute( r( 0, 3 ), 'uppercase', true );
-			expect( getCaseText( root ) ).to.equal( 'EBCdf' );
-
-			undo._execute( batch0 );
-			expect( getCaseText( root ) ).to.equal( 'BCdEf' );
-
-			// Let's simulate splitting the delta by updating the history by hand.
-			let attrHistoryDelta = doc.history.getDelta( 2 )[ 0 ];
-			let attrDelta1 = new AttributeDelta();
-			attrDelta1.addOperation( attrHistoryDelta.operations[ 0 ] );
-			let attrDelta2 = new AttributeDelta();
-			attrDelta2.addOperation( attrHistoryDelta.operations[ 1 ] );
-			doc.history.updateDelta( 2, [ attrDelta1, attrDelta2 ] );
-
-			undo._execute( batch1 );
-			// After this execution, undo algorithm should update both `attrDelta1` and `attrDelta2` with new
-			// versions, that have incremented offsets.
-			expect( getCaseText( root ) ).to.equal( 'aBCdEf' );
-
-			undo._execute( batch2 );
-			// This execution checks whether undo algorithm correctly updated deltas in previous execution
-			// and also whether it correctly "reads" both deltas from history.
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
-		} );
-
-		it( 'merges touching ranges when restoring selection', () => {
-			root.appendChildren( new Text( 'abcdef' ) );
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
-
-			editor.document.selection.setRanges( [ r( 1, 4 ) ] );
-			let batch0 = doc.batch();
-			undo.addBatch( batch0 );
-			batch0.setAttribute( r( 1, 4 ), 'uppercase', true );
-			expect( getCaseText( root ) ).to.equal( 'aBCDef' );
-
-			editor.document.selection.setRanges( [ r( 3, 4 ) ] );
-			let batch1 = doc.batch();
-			undo.addBatch( batch1 );
-			batch1.move( r( 3, 4 ), p( 1 ) );
-			expect( getCaseText( root ) ).to.equal( 'aDBCef' );
-
-			undo._execute( batch0 );
+	afterEach( () => {
+		undo.destroy();
+	} );
 
-			// After undo-attr: acdbef <--- "cdb" should be selected, it would look weird if only "cd" or "b" is selected
-			// but the whole unbroken part "cdb" changed attribute.
-			expect( getCaseText( root ) ).to.equal( 'adbcef' );
-			expect( editor.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true;
+	describe( 'UndoCommand', () => {
+		const p = pos => new Position( root, [].concat( pos ) );
+		const r = ( a, b ) => new Range( p( a ), p( b ) );
+
+		describe( '_execute', () => {
+			let batch0, batch1, batch2, batch3;
+
+			beforeEach( () => {
+				/*
+				 [root]
+				 - {}
+				 */
+				editor.document.selection.setRanges( [ r( 0, 0 ) ] );
+				batch0 = doc.batch();
+				undo.addBatch( batch0 );
+				batch0.insert( p( 0 ), 'foobar' );
+				/*
+				 [root]
+				 - f
+				 - o
+				 - o
+				 - b
+				 - a
+				 - r{}
+				 */
+				// Let's make things spicy and this time, make a backward selection.
+				editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
+				batch1 = doc.batch();
+				undo.addBatch( batch1 );
+				batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
+				/*
+				 [root]
+				 - f
+				 - o
+				 - {o (key: value)
+				 - b} (key: value)
+				 - a
+				 - r
+				 */
+				editor.document.selection.setRanges( [ r( 1, 3 ) ] );
+				batch2 = doc.batch();
+				undo.addBatch( batch2 );
+				batch2.move( r( 1, 3 ), p( 6 ) );
+				/*
+				 [root]
+				 - f
+				 - b (key: value)
+				 - a
+				 - r
+				 - {o
+				 - o} (key: value)
+				 */
+				editor.document.selection.setRanges( [ r( 1, 4 ) ] );
+				batch3 = doc.batch();
+				undo.addBatch( batch3 );
+				batch3.wrap( r( 1, 4 ), 'p' );
+				/*
+				 [root]
+				 - f
+				 - [p]
+				 	- {b (key: value)
+				 	- a
+				 	- r}
+				 - o
+				 - o (key: value)
+				 */
+				editor.document.selection.setRanges( [ r( 0, 1 ) ] );
+				batch2.move( r( 0, 1 ), p( 3 ) );
+				/*
+				 [root]
+				 - [p]
+				 	- b (key: value)
+				 	- a
+				 	- r
+				 - o
+				 - f
+				 - o{} (key: value)
+				 */
+				editor.document.selection.setRanges( [ r( 4, 4 ) ] );
+			} );
+
+			it( 'should revert changes done by deltas from the batch that was most recently added to the command stack', () => {
+				undo._execute();
+
+				// Selection is restored. Wrap is removed:
+				/*
+				 [root]
+				 - {b (key: value)
+				 - a
+				 - r}
+				 - o
+				 - f
+				 - o (key: value)
+				 */
+
+				expect( getText( root ) ).to.equal( 'barofo' );
+				expect( itemAt( root, 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+
+				undo._execute();
+
+				// Two moves are removed:
+				/*
+				 [root]
+				 - f
+				 - {o
+				 - o} (key: value)
+				 - b (key: value)
+				 - a
+				 - r
+				 */
+
+				expect( getText( root ) ).to.equal( 'foobar' );
+				expect( itemAt( root, 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
+				expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 3 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+
+				undo._execute();
+
+				// Set attribute is undone:
+				/*
+				 [root]
+				 - f
+				 - o
+				 - {o
+				 - b}
+				 - a
+				 - r
+				 */
+
+				expect( getText( root ) ).to.equal( 'foobar' );
+				expect( itemAt( root, 2 ).hasAttribute( 'key' ) ).to.be.false;
+				expect( itemAt( root, 3 ).hasAttribute( 'key' ) ).to.be.false;
+
+				expect( editor.document.selection.getFirstRange().isEqual( r( 2, 4 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.true;
+
+				undo._execute();
+
+				// Insert is undone:
+				/*
+				 [root]
+				 */
+
+				expect( root.childCount ).to.equal( 0 );
+				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
+			} );
+
+			it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert set attribute)', () => {
+				undo._execute( batch1 );
+				// Remove attribute:
+				/*
+				 [root]
+				 - [p]
+				 	- b
+				 	- a
+				 	- r
+				 - o
+				 - f
+				 - o
+				 */
+
+				expect( itemAt( root, 0 ).name ).to.equal( 'p' );
+				expect( getText( root.getChild( 0 ) ) ).to.equal( 'bar' );
+				expect( root.getChild( 1 ).data ).to.equal( 'ofo' );
+
+				expect( itemAt( root.getChild( 0 ), 0 ).hasAttribute( 'key' ) ).to.be.false;
+				expect( itemAt( root, 2 ).hasAttribute( 'key' ) ).to.be.false;
+				expect( itemAt( root, 3 ).hasAttribute( 'key' ) ).to.be.false;
+
+				// Selection is only partially restored because the range got broken.
+				// The selection would have to best on letter "b" and letter "o", but it is set only on letter "b".
+				expect( editor.document.selection.getFirstRange().isEqual( r( [ 0, 0 ], [ 0, 1 ] ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.true;
+			} );
+
+			it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert insert foobar)', () => {
+				undo._execute( batch0 );
+				// Remove foobar:
+				/*
+				 [root]
+				 - [p]
+				 */
+
+				// The `P` element wasn't removed because it wasn`t added by undone batch.
+				// It would be perfect if the `P` got removed aswell because wrapping was on removed nodes.
+				// But this would need a lot of logic / hardcoded ifs or a post-fixer.
+				expect( root.childCount ).to.equal( 1 );
+				expect( itemAt( root, 0 ).name ).to.equal( 'p' );
+
+				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+
+				undo._execute( batch1 );
+				// Remove attributes.
+				// This does nothing in the `root` because attributes were set on nodes that already got removed.
+				// But those nodes should change in the graveyard and we can check them there.
+
+				expect( root.childCount ).to.equal( 1 );
+				expect( itemAt( root, 0 ).name ).to.equal( 'p' );
+
+				// Operations for undoing that batch were working on graveyard so document selection should not change.
+				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+
+				expect( doc.graveyard.getChild( 0 ).maxOffset ).to.equal( 6 );
+
+				for ( let char of doc.graveyard._children ) {
+					expect( char.hasAttribute( 'key' ) ).to.be.false;
+				}
+
+				// Let's undo wrapping. This should leave us with empty root.
+				undo._execute( batch3 );
+				expect( root.maxOffset ).to.equal( 0 );
+
+				// Once again transformed range ends up in the graveyard.
+				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
+				expect( editor.document.selection.isBackward ).to.be.false;
+			} );
 		} );
 
-		it( 'does nothing (and not crashes) if delta to undo is no longer in history', () => {
-			// Also an edgy situation but it may come up if other plugins use `CompressedHistory` API.
-			root.appendChildren( new Text( 'abcdef' ) );
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
+		// Some tests to ensure 100% CC and proper behavior in edge cases.
+		describe( 'edge cases', () => {
+			function getCaseText( root ) {
+				let text = '';
 
-			editor.document.selection.setRanges( [ r( 0, 1 ) ] );
-			let batch0 = doc.batch();
-			undo.addBatch( batch0 );
-			batch0.setAttribute( r( 0, 1 ), 'uppercase', true );
-			expect( getCaseText( root ) ).to.equal( 'Abcdef' );
+				for ( let i = 0; i < root.childCount; i++ ) {
+					let node = root.getChild( i );
+					text += node.getAttribute( 'uppercase' ) ? node.data.toUpperCase() : node.data;
+				}
 
-			doc.history.removeDelta( 0 );
-			root.getChild( 0 ).removeAttribute( 'uppercase' );
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
-
-			undo._execute();
+				return text;
+			}
 
-			// Nothing happened. We are still alive.
-			expect( getCaseText( root ) ).to.equal( 'abcdef' );
+			it( 'correctly handles deltas in compressed history that were earlier updated into multiple deltas (or split when undoing)', () => {
+				// In this case we assume that one of the deltas in compressed history was updated to two deltas.
+				// This is a tricky edge case because it is almost impossible to come up with convincing scenario that produces it.
+				// At the moment of writing this test and comment, only Undo feature uses `CompressedHistory#updateDelta`.
+				// Because deltas that "stays" in history are transformed with `isStrong` flag set to `false`, `MoveOperation`
+				// won't get split and `AttributeDelta` can hold multiple `AttributeOperation` in it. So using most common deltas
+				// (`InsertDelta`, `RemoveDelta`, `MoveDelta`, `AttributeDelta`) and undo it's impossible to get to this edge case.
+				// Still there might be some weird scenarios connected with OT / Undo / Collaborative Editing / other deltas /
+				// fancy 3rd party plugin where it may come up, so it's better to be safe than sorry.
+
+				root.appendChildren( new Text( 'abcdef' ) );
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+
+				editor.document.selection.setRanges( [ r( 1, 4 ) ] );
+				let batch0 = doc.batch();
+				undo.addBatch( batch0 );
+				batch0.move( r( 1, 4 ), p( 5 ) );
+				expect( getCaseText( root ) ).to.equal( 'aebcdf' );
+
+				editor.document.selection.setRanges( [ r( 1, 1 ) ]  );
+				let batch1 = doc.batch();
+				undo.addBatch( batch1 );
+				batch1.remove( r( 0, 1 ) );
+				expect( getCaseText( root ) ).to.equal( 'ebcdf' );
+
+				editor.document.selection.setRanges( [ r( 0, 3 ) ] );
+				let batch2 = doc.batch();
+				undo.addBatch( batch2 );
+				batch2.setAttribute( r( 0, 3 ), 'uppercase', true );
+				expect( getCaseText( root ) ).to.equal( 'EBCdf' );
+
+				undo._execute( batch0 );
+				expect( getCaseText( root ) ).to.equal( 'BCdEf' );
+
+				// Let's simulate splitting the delta by updating the history by hand.
+				let attrHistoryDelta = doc.history.getDelta( 2 )[ 0 ];
+				let attrDelta1 = new AttributeDelta();
+				attrDelta1.addOperation( attrHistoryDelta.operations[ 0 ] );
+				let attrDelta2 = new AttributeDelta();
+				attrDelta2.addOperation( attrHistoryDelta.operations[ 1 ] );
+				doc.history.updateDelta( 2, [ attrDelta1, attrDelta2 ] );
+
+				undo._execute( batch1 );
+				// After this execution, undo algorithm should update both `attrDelta1` and `attrDelta2` with new
+				// versions, that have incremented offsets.
+				expect( getCaseText( root ) ).to.equal( 'aBCdEf' );
+
+				undo._execute( batch2 );
+				// This execution checks whether undo algorithm correctly updated deltas in previous execution
+				// and also whether it correctly "reads" both deltas from history.
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+			} );
+
+			it( 'merges touching ranges when restoring selection', () => {
+				root.appendChildren( new Text( 'abcdef' ) );
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+
+				editor.document.selection.setRanges( [ r( 1, 4 ) ] );
+				let batch0 = doc.batch();
+				undo.addBatch( batch0 );
+				batch0.setAttribute( r( 1, 4 ), 'uppercase', true );
+				expect( getCaseText( root ) ).to.equal( 'aBCDef' );
+
+				editor.document.selection.setRanges( [ r( 3, 4 ) ] );
+				let batch1 = doc.batch();
+				undo.addBatch( batch1 );
+				batch1.move( r( 3, 4 ), p( 1 ) );
+				expect( getCaseText( root ) ).to.equal( 'aDBCef' );
+
+				undo._execute( batch0 );
+
+				// After undo-attr: acdbef <--- "cdb" should be selected, it would look weird if only "cd" or "b" is selected
+				// but the whole unbroken part "cdb" changed attribute.
+				expect( getCaseText( root ) ).to.equal( 'adbcef' );
+				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true;
+			} );
+
+			it( 'does nothing (and not crashes) if delta to undo is no longer in history', () => {
+				// Also an edgy situation but it may come up if other plugins use `CompressedHistory` API.
+				root.appendChildren( new Text( 'abcdef' ) );
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+
+				editor.document.selection.setRanges( [ r( 0, 1 ) ] );
+				let batch0 = doc.batch();
+				undo.addBatch( batch0 );
+				batch0.setAttribute( r( 0, 1 ), 'uppercase', true );
+				expect( getCaseText( root ) ).to.equal( 'Abcdef' );
+
+				doc.history.removeDelta( 0 );
+				root.getChild( 0 ).removeAttribute( 'uppercase' );
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+
+				undo._execute();
+
+				// Nothing happened. We are still alive.
+				expect( getCaseText( root ) ).to.equal( 'abcdef' );
+			} );
 		} );
 	} );
 } );

+ 212 - 210
packages/ckeditor5-undo/tests/undoengine-integration.js

@@ -10,303 +10,305 @@ import UndoEngine from 'ckeditor5/undo/undoengine.js';
 
 import { setData, getData } from 'ckeditor5/engine/dev-utils/model.js';
 
-let editor, doc, root;
-
-beforeEach( () => {
-	return ModelTestEditor.create( {
-			plugins: [ UndoEngine ]
-		} )
-		.then( newEditor => {
-			editor = newEditor;
-			doc = editor.document;
-			doc.schema.registerItem( 'p', '$block' );
-			root = doc.getRoot();
-		} );
-} );
+describe( 'UndoEngine integration', () => {
+	let editor, doc, root;
+
+	beforeEach( () => {
+		return ModelTestEditor.create( {
+				plugins: [ UndoEngine ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.document;
+				doc.schema.registerItem( 'p', '$block' );
+				root = doc.getRoot();
+			} );
+	} );
 
-function setSelection( pathA, pathB ) {
-	doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
-}
+	function setSelection( pathA, pathB ) {
+		doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
+	}
 
-function input( input ) {
-	setData( doc, input );
-}
+	function input( input ) {
+		setData( doc, input );
+	}
 
-function output( output ) {
-	expect( getData( doc ) ).to.equal( output );
-}
+	function output( output ) {
+		expect( getData( doc ) ).to.equal( output );
+	}
 
-function undoDisabled() {
-	expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
-}
+	function undoDisabled() {
+		expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
+	}
 
-describe( 'UndoEngine integration', () => {
-	describe( 'adding and removing content', () => {
-		it( 'add and undo', () => {
-			input( '<p>fo[]o</p><p>bar</p>' );
+	describe( 'UndoEngine integration', () => {
+		describe( 'adding and removing content', () => {
+			it( 'add and undo', () => {
+				input( '<p>fo[]o</p><p>bar</p>' );
 
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz[]o</p><p>bar</p>' );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				output( '<p>fozzz[]o</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'multiple adding and undo', () => {
-			input( '<p>fo[]o</p><p>bar</p>' );
+			it( 'multiple adding and undo', () => {
+				input( '<p>fo[]o</p><p>bar</p>' );
 
-			doc.batch()
-				.insert( doc.selection.getFirstPosition(), 'zzz' )
-				.insert( new Position( root, [ 1, 0 ] ), 'xxx' );
-			output( '<p>fozzz[]o</p><p>xxxbar</p>' );
+				doc.batch()
+					.insert( doc.selection.getFirstPosition(), 'zzz' )
+					.insert( new Position( root, [ 1, 0 ] ), 'xxx' );
+				output( '<p>fozzz[]o</p><p>xxxbar</p>' );
 
-			setSelection( [ 1, 0 ], [ 1, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
-			output( '<p>fozzzo</p><p>yyy[]xxxbar</p>' );
+				setSelection( [ 1, 0 ], [ 1, 0 ] );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+				output( '<p>fozzzo</p><p>yyy[]xxxbar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p>[]xxxbar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fozzzo</p><p>[]xxxbar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'multiple adding mixed with undo', () => {
-			input( '<p>fo[]o</p><p>bar</p>' );
+			it( 'multiple adding mixed with undo', () => {
+				input( '<p>fo[]o</p><p>bar</p>' );
 
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz[]o</p><p>bar</p>' );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				output( '<p>fozzz[]o</p><p>bar</p>' );
 
-			setSelection( [ 1, 0 ], [ 1, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
-			output( '<p>fozzzo</p><p>yyy[]bar</p>' );
+				setSelection( [ 1, 0 ], [ 1, 0 ] );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+				output( '<p>fozzzo</p><p>yyy[]bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p>[]bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fozzzo</p><p>[]bar</p>' );
 
-			setSelection( [ 0, 0 ], [ 0, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
-			output( '<p>xxx[]fozzzo</p><p>bar</p>' );
+				setSelection( [ 0, 0 ], [ 0, 0 ] );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
+				output( '<p>xxx[]fozzzo</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>[]fozzzo</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>[]fozzzo</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'multiple remove and undo', () => {
-			input( '<p>[]foo</p><p>bar</p>' );
+			it( 'multiple remove and undo', () => {
+				input( '<p>[]foo</p><p>bar</p>' );
 
-			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
-			output( '<p>[]o</p><p>bar</p>' );
+				doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+				output( '<p>[]o</p><p>bar</p>' );
 
-			setSelection( [ 1, 1 ], [ 1, 1 ] );
-			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
-			output( '<p>o</p><p>b[]</p>' );
+				setSelection( [ 1, 1 ], [ 1, 1 ] );
+				doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+				output( '<p>o</p><p>b[]</p>' );
 
-			editor.execute( 'undo' );
-			// Here is an edge case that selection could be before or after `ar`.
-			output( '<p>o</p><p>b[]ar</p>' );
+				editor.execute( 'undo' );
+				// Here is an edge case that selection could be before or after `ar`.
+				output( '<p>o</p><p>b[]ar</p>' );
 
-			editor.execute( 'undo' );
-			// As above.
-			output( '<p>[]foo</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				// As above.
+				output( '<p>[]foo</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'add and remove different parts and undo', () => {
-			input( '<p>fo[]o</p><p>bar</p>' );
+			it( 'add and remove different parts and undo', () => {
+				input( '<p>fo[]o</p><p>bar</p>' );
 
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz[]o</p><p>bar</p>' );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				output( '<p>fozzz[]o</p><p>bar</p>' );
 
-			setSelection( [ 1, 2 ], [ 1, 2 ] );
-			doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
-			output( '<p>fozzzo</p><p>b[]r</p>' );
+				setSelection( [ 1, 2 ], [ 1, 2 ] );
+				doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
+				output( '<p>fozzzo</p><p>b[]r</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p>ba[]r</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fozzzo</p><p>ba[]r</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'add and remove same part and undo', () => {
-			input( '<p>fo[]o</p><p>bar</p>' );
+			it( 'add and remove same part and undo', () => {
+				input( '<p>fo[]o</p><p>bar</p>' );
 
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz[]o</p><p>bar</p>' );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				output( '<p>fozzz[]o</p><p>bar</p>' );
 
-			doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fozzz[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fozzz[]o</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[]o</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[]o</p><p>bar</p>' );
 
-			undoDisabled();
+				undoDisabled();
+			} );
 		} );
-	} );
 
-	describe( 'moving', () => {
-		it( 'move same content twice then undo', () => {
-			input( '<p>f[o]z</p><p>bar</p>' );
+		describe( 'moving', () => {
+			it( 'move same content twice then undo', () => {
+				input( '<p>f[o]z</p><p>bar</p>' );
 
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
-			output( '<p>fz</p><p>[o]bar</p>' );
+				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+				output( '<p>fz</p><p>[o]bar</p>' );
 
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
-			output( '<p>fz[o]</p><p>bar</p>' );
+				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
+				output( '<p>fz[o]</p><p>bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fz</p><p>[o]bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fz</p><p>[o]bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>f[o]z</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>f[o]z</p><p>bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'move content and new parent then undo', () => {
-			input( '<p>f[o]z</p><p>bar</p>' );
+			it( 'move content and new parent then undo', () => {
+				input( '<p>f[o]z</p><p>bar</p>' );
 
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
-			output( '<p>fz</p><p>[o]bar</p>' );
+				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+				output( '<p>fz</p><p>[o]bar</p>' );
 
-			setSelection( [ 1 ], [ 2 ] );
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
-			output( '[<p>obar</p>]<p>fz</p>' );
+				setSelection( [ 1 ], [ 2 ] );
+				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+				output( '[<p>obar</p>]<p>fz</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fz</p>[<p>obar</p>]' );
+				editor.execute( 'undo' );
+				output( '<p>fz</p>[<p>obar</p>]' );
 
-			editor.execute( 'undo' );
-			output( '<p>f[o]z</p><p>bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>f[o]z</p><p>bar</p>' );
 
-			undoDisabled();
+				undoDisabled();
+			} );
 		} );
-	} );
 
-	describe( 'attributes with other', () => {
-		it( 'attributes then insert inside then undo', () => {
-			input( '<p>fo[ob]ar</p>' );
+		describe( 'attributes with other', () => {
+			it( 'attributes then insert inside then undo', () => {
+				input( '<p>fo[ob]ar</p>' );
 
-			doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true );
-			output( '<p>fo[<$text bold="true">ob</$text>]ar</p>' );
+				doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true );
+				output( '<p>fo[<$text bold="true">ob</$text>]ar</p>' );
 
-			setSelection( [ 0, 3 ], [ 0, 3 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</p>' );
-			expect( doc.selection.getAttribute( 'bold' ) ).to.true;
+				setSelection( [ 0, 3 ], [ 0, 3 ] );
+				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				output( '<p>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</p>' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.true;
 
-			editor.execute( 'undo' );
-			output( '<p>fo<$text bold="true">o[]b</$text>ar</p>' );
-			expect( doc.selection.getAttribute( 'bold' ) ).to.true;
+				editor.execute( 'undo' );
+				output( '<p>fo<$text bold="true">o[]b</$text>ar</p>' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.true;
 
-			editor.execute( 'undo' );
-			output( '<p>fo[ob]ar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[ob]ar</p>' );
 
-			undoDisabled();
+				undoDisabled();
+			} );
 		} );
-	} );
 
-	describe( 'wrapping, unwrapping, merging, splitting', () => {
-		it( 'wrap and undo', () => {
-			doc.schema.allow( { name: '$text', inside: '$root' } );
-			input( 'fo[zb]ar' );
+		describe( 'wrapping, unwrapping, merging, splitting', () => {
+			it( 'wrap and undo', () => {
+				doc.schema.allow( { name: '$text', inside: '$root' } );
+				input( 'fo[zb]ar' );
 
-			doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
-			output( 'fo<p>[zb]</p>ar' );
+				doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
+				output( 'fo<p>[zb]</p>ar' );
 
-			editor.execute( 'undo' );
-			output( 'fo[zb]ar' );
+				editor.execute( 'undo' );
+				output( 'fo[zb]ar' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'wrap, move and undo', () => {
-			doc.schema.allow( { name: '$text', inside: '$root' } );
-			input( 'fo[zb]ar' );
+			it( 'wrap, move and undo', () => {
+				doc.schema.allow( { name: '$text', inside: '$root' } );
+				input( 'fo[zb]ar' );
 
-			doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
-			// Would be better if selection was inside P.
-			output( 'fo<p>[zb]</p>ar' );
+				doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
+				// Would be better if selection was inside P.
+				output( 'fo<p>[zb]</p>ar' );
 
-			setSelection( [ 2, 0 ], [ 2, 1 ] );
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
-			output( '[z]fo<p>b</p>ar' );
+				setSelection( [ 2, 0 ], [ 2, 1 ] );
+				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+				output( '[z]fo<p>b</p>ar' );
 
-			editor.execute( 'undo' );
-			output( 'fo<p>[z]b</p>ar' );
+				editor.execute( 'undo' );
+				output( 'fo<p>[z]b</p>ar' );
 
-			editor.execute( 'undo' );
-			output( 'fo[zb]ar' );
+				editor.execute( 'undo' );
+				output( 'fo[zb]ar' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'unwrap and undo', () => {
-			input( '<p>foo[]bar</p>' );
+			it( 'unwrap and undo', () => {
+				input( '<p>foo[]bar</p>' );
 
-			doc.batch().unwrap( doc.selection.getFirstPosition().parent );
-			output( 'foo[]bar' );
+				doc.batch().unwrap( doc.selection.getFirstPosition().parent );
+				output( 'foo[]bar' );
 
-			editor.execute( 'undo' );
-			output( '<p>foo[]bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>foo[]bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'merge and undo', () => {
-			input( '<p>foo</p><p>[]bar</p>' );
+			it( 'merge and undo', () => {
+				input( '<p>foo</p><p>[]bar</p>' );
 
-			doc.batch().merge( new Position( root, [ 1 ] ) );
-			// Because selection is stuck with <p> it ends up in graveyard. We have to manually move it to correct node.
-			setSelection( [ 0, 3 ], [ 0, 3 ] );
-			output( '<p>foo[]bar</p>' );
+				doc.batch().merge( new Position( root, [ 1 ] ) );
+				// Because selection is stuck with <p> it ends up in graveyard. We have to manually move it to correct node.
+				setSelection( [ 0, 3 ], [ 0, 3 ] );
+				output( '<p>foo[]bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>foo</p><p>[]bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>foo</p><p>[]bar</p>' );
 
-			undoDisabled();
-		} );
+				undoDisabled();
+			} );
 
-		it( 'split and undo', () => {
-			input( '<p>foo[]bar</p>' );
+			it( 'split and undo', () => {
+				input( '<p>foo[]bar</p>' );
 
-			doc.batch().split( doc.selection.getFirstPosition() );
-			// Because selection is stuck with <p> it ends up in wrong node. We have to manually move it to correct node.
-			setSelection( [ 1, 0 ], [ 1, 0 ] );
-			output( '<p>foo</p><p>[]bar</p>' );
+				doc.batch().split( doc.selection.getFirstPosition() );
+				// Because selection is stuck with <p> it ends up in wrong node. We have to manually move it to correct node.
+				setSelection( [ 1, 0 ], [ 1, 0 ] );
+				output( '<p>foo</p><p>[]bar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>foo[]bar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>foo[]bar</p>' );
 
-			undoDisabled();
+				undoDisabled();
+			} );
 		} );
-	} );
 
-	describe( 'other edge cases', () => {
-		it( 'deleteContent between two nodes', () => {
-			input( '<p>fo[o</p><p>b]ar</p>' );
+		describe( 'other edge cases', () => {
+			it( 'deleteContent between two nodes', () => {
+				input( '<p>fo[o</p><p>b]ar</p>' );
 
-			editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } );
-			output( '<p>fo[]ar</p>' );
+				editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } );
+				output( '<p>fo[]ar</p>' );
 
-			editor.execute( 'undo' );
-			output( '<p>fo[o</p><p>b]ar</p>' );
+				editor.execute( 'undo' );
+				output( '<p>fo[o</p><p>b]ar</p>' );
+			} );
 		} );
 	} );
 } );

+ 47 - 45
packages/ckeditor5-undo/tests/undoengine.js

@@ -7,69 +7,71 @@ import ModelTestEditor from 'tests/core/_utils/modeltesteditor.js';
 import Position from 'ckeditor5/engine/model/position.js';
 import UndoEngine from 'ckeditor5/undo/undoengine.js';
 
-let editor, undo, batch, doc, root;
-
-beforeEach( () => {
-	editor = new ModelTestEditor();
+describe( 'UndoEngine', () => {
+	let editor, undo, batch, doc, root;
 
-	doc = editor.document;
-	batch = doc.batch();
-	root = doc.getRoot();
+	beforeEach( () => {
+		editor = new ModelTestEditor();
 
-	undo = new UndoEngine( editor );
-	undo.init();
-} );
+		doc = editor.document;
+		batch = doc.batch();
+		root = doc.getRoot();
 
-afterEach( () => {
-	undo.destroy();
-} );
+		undo = new UndoEngine( editor );
+		undo.init();
+	} );
 
-describe( 'UndoEngine', () => {
-	it( 'should register undo command and redo command', () => {
-		expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
-		expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );
+	afterEach( () => {
+		undo.destroy();
 	} );
 
-	it( 'should add a batch to undo command and clear redo stack, if it\'s type is "default"', () => {
-		sinon.spy( undo._undoCommand, 'addBatch' );
-		sinon.spy( undo._redoCommand, 'clearStack' );
+	describe( 'UndoEngine', () => {
+		it( 'should register undo command and redo command', () => {
+			expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
+			expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );
+		} );
 
-		expect( undo._undoCommand.addBatch.called ).to.be.false;
-		expect( undo._redoCommand.clearStack.called ).to.be.false;
+		it( 'should add a batch to undo command and clear redo stack, if it\'s type is "default"', () => {
+			sinon.spy( undo._undoCommand, 'addBatch' );
+			sinon.spy( undo._redoCommand, 'clearStack' );
 
-		batch.insert( new Position( root, [ 0 ] ), 'foobar' );
+			expect( undo._undoCommand.addBatch.called ).to.be.false;
+			expect( undo._redoCommand.clearStack.called ).to.be.false;
 
-		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-		expect( undo._redoCommand.clearStack.calledOnce ).to.be.true;
-	} );
+			batch.insert( new Position( root, [ 0 ] ), 'foobar' );
 
-	it( 'should add each batch only once', () => {
-		sinon.spy( undo._undoCommand, 'addBatch' );
+			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+			expect( undo._redoCommand.clearStack.calledOnce ).to.be.true;
+		} );
 
-		batch.insert( new Position( root, [ 0 ] ), 'foobar' ).insert( new Position( root, [ 0 ] ), 'foobar' );
+		it( 'should add each batch only once', () => {
+			sinon.spy( undo._undoCommand, 'addBatch' );
 
-		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-	} );
+			batch.insert( new Position( root, [ 0 ] ), 'foobar' ).insert( new Position( root, [ 0 ] ), 'foobar' );
 
-	it( 'should add a batch to undo command, if it\'s type is undo and it comes from redo command', () => {
-		sinon.spy( undo._undoCommand, 'addBatch' );
-		sinon.spy( undo._redoCommand, 'clearStack' );
+			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+		} );
 
-		undo._redoCommand._createdBatches.add( batch );
+		it( 'should add a batch to undo command, if it\'s type is undo and it comes from redo command', () => {
+			sinon.spy( undo._undoCommand, 'addBatch' );
+			sinon.spy( undo._redoCommand, 'clearStack' );
 
-		batch.insert( new Position( root, [ 0 ] ), 'foobar' );
+			undo._redoCommand._createdBatches.add( batch );
 
-		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-		expect( undo._redoCommand.clearStack.called ).to.be.false;
-	} );
+			batch.insert( new Position( root, [ 0 ] ), 'foobar' );
+
+			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+			expect( undo._redoCommand.clearStack.called ).to.be.false;
+		} );
 
-	it( 'should add a batch to redo command on undo revert event', () => {
-		sinon.spy( undo._redoCommand, 'addBatch' );
-		sinon.spy( undo._redoCommand, 'clearStack' );
+		it( 'should add a batch to redo command on undo revert event', () => {
+			sinon.spy( undo._redoCommand, 'addBatch' );
+			sinon.spy( undo._redoCommand, 'clearStack' );
 
-		undo._undoCommand.fire( 'revert', null, batch );
+			undo._undoCommand.fire( 'revert', null, batch );
 
-		expect( undo._redoCommand.addBatch.calledOnce ).to.be.true;
-		expect( undo._redoCommand.clearStack.called ).to.be.false;
+			expect( undo._redoCommand.addBatch.calledOnce ).to.be.true;
+			expect( undo._redoCommand.clearStack.called ).to.be.false;
+		} );
 	} );
 } );