Jelajahi Sumber

Added LiveRange, changed LivePosition stickiness now also affects moving-out nodes.

Szymon Cofalik 10 tahun lalu
induk
melakukan
af0d6020bc

+ 7 - 6
packages/ckeditor5-engine/src/treemodel/liveposition.js

@@ -61,7 +61,7 @@ CKEDITOR.define( [
 		}
 
 		/**
-		 * Unbinds all events previously binded by LivePosition. Use it whenever you don't need LivePosition instance
+		 * Unbinds all events previously bound by LivePosition. Use it whenever you don't need LivePosition instance
 		 * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
 		 * referring to it).
 		 */
@@ -75,7 +75,6 @@ CKEDITOR.define( [
 	 *
 	 * @private
 	 * @method bindWithDocument
-	 * @private
 	 */
 	function bindWithDocument() {
 		/*jshint validthis: true */
@@ -108,10 +107,12 @@ CKEDITOR.define( [
 			case 'reinsert':
 				let originalRange = Range.createFromPositionAndShift( position, howMany );
 
-				if ( originalRange.containsPosition( this ) ) {
-					// We can't use .getTransformedByMove() because it would not combine
-					// path if LivePosition is at the same tree-level as range.
-					// Here, we always want to combine.
+				let gotMoved = originalRange.containsPosition( this ) ||
+					( originalRange.start.isEqual( this ) && !this.stickToLeft ) ||
+					( originalRange.end.isEqual( this ) && this.stickToLeft );
+
+				// We can't use .getTransformedByMove() because we have a different if-condition.
+				if ( gotMoved ) {
 					transformed = this._getCombined( position, range.start );
 				} else {
 					transformed = this.getTransformedByMove( position, range.start, howMany, !this.stickToLeft );

+ 116 - 0
packages/ckeditor5-engine/src/treemodel/liverange.js

@@ -0,0 +1,116 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'treemodel/range',
+	'treemodel/liveposition',
+	'emittermixin',
+	'utils'
+], ( Range, LivePosition, EmitterMixin, utils ) => {
+	/**
+	 * LiveRange is a Range in the Tree Model that updates itself as the tree changes. It may be used as a bookmark.
+	 * **Note:** Be very careful when dealing with LiveRange. Each LiveRange instance bind events that might
+	 * have to be unbound. Use {@link #detach} whenever you don't need LiveRange anymore.
+	 *
+	 * @class treeModel.LiveRange
+	 */
+	class LiveRange extends Range {
+		/**
+		 * Creates a smart range.
+		 *
+		 * @see {treeModel.Range}
+		 * @constructor
+		 */
+		constructor( start, end ) {
+			super( start, end );
+
+			bindWithDocument.call( this );
+		}
+
+		/**
+		 * Creates and returns a new range which is equal to this LiveRange. Returned object may be
+		 * an instance of Range or LiveRange, depending on passed argument.
+		 *
+		 * @param {Boolean} [makeLiveRange] Flag representing whether new object should be "live". Defaults to false.
+		 * @returns {treeModel.Range|treeModel.LiveRange}
+		 */
+		clone( makeLiveRange ) {
+			if ( makeLiveRange ) {
+				return new LiveRange( this.start.clone(), this.end.clone() );
+			} else {
+				return super.clone();
+			}
+		}
+
+		/**
+		 * Unbinds all events previously bound by LiveRange. Use it whenever you don't need LiveRange instance
+		 * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
+		 * referring to it).
+		 */
+		detach() {
+			this.start.detach();
+			this.end.detach();
+			this.stopListening();
+		}
+	}
+
+	/**
+	 * Binds this LiveRange to the {@link treeModel.Document} that owns this range.
+	 *
+	 * @private
+	 * @method bindWithDocument
+	 */
+	function bindWithDocument() {
+		/*jshint validthis: true */
+
+		if ( !( this.start instanceof LivePosition ) ) {
+			this.start = new LivePosition( this.start.root, this.start.path.slice(), false );
+		}
+
+		if ( !( this.end instanceof LivePosition ) ) {
+			this.end = new LivePosition( this.end.root, this.end.path.slice(), true );
+		}
+
+		this.listenTo( this.root.document, 'change', ( e, t, r, p ) => fixBoundaries.call( this, t, r, p ), this );
+	}
+
+	/**
+	 * LiveRange is partially updated by "automatic" updates of LivePositions that are boundaries of LiveRange.
+	 * However there are cases when the boundaries have to be fixed because they end up in wrong places.
+	 *
+	 * @private
+	 * @method fixBoundaries
+	 * @param {String} type Type of changes applied to the Tree Model.
+	 * @param {treeModel.Range} range Range containing the result of applied change.
+	 * @param {treeModel.Position} [position] Additional position parameter provided by some change events.
+	 */
+	function fixBoundaries( type, range, position ) {
+		/*jshint validthis: true */
+
+		if ( type == 'move' || type == 'remove' || type == 'reinsert' ) {
+			let containsStart = range.containsPosition( this.start ) || range.start.isEqual( this.start );
+			let containsEnd = range.containsPosition( this.end ) || range.end.isEqual( this.end );
+			position = position.getTransformedByInsertion( range.start, range.end.offset - range.start.offset, true );
+
+			// If the range contains both start and end, don't do anything - LivePositions that are boundaries of
+			// this LiveRange are in correct places, they got correctly transformed.
+			if ( containsStart && !containsEnd && !range.end.isTouching( position ) ) {
+				this.start.path = position.path.slice();
+				this.start.root = position.root;
+			}
+
+			if ( containsEnd && !containsStart && !range.start.isTouching( position ) ) {
+				this.end.path = position.path.slice();
+				this.end.root = position.root;
+			}
+		}
+	}
+
+	utils.extend( LiveRange.prototype, EmitterMixin );
+
+	return LiveRange;
+} );

+ 446 - 0
packages/ckeditor5-engine/tests/treemodel/liverange.js

@@ -0,0 +1,446 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel */
+
+'use strict';
+
+const modules = bender.amd.require(
+	'treemodel/document',
+	'treemodel/element',
+	'treemodel/position',
+	'treemodel/liverange',
+	'treemodel/range',
+	'emittermixin'
+);
+
+describe( 'LiveRange', () => {
+	let Document, Element, Position, LiveRange, Range, EmitterMixin;
+	let doc, root, ul, p;
+
+	before( () => {
+		Document = modules[ 'treemodel/document' ];
+		Element = modules[ 'treemodel/element' ];
+		Position = modules[ 'treemodel/position' ];
+		LiveRange = modules[ 'treemodel/liverange' ];
+		Range = modules[ 'treemodel/range' ];
+		EmitterMixin = modules.emittermixin;
+
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		let lis = [
+			new Element( 'li', [], 'aaaaaaaaaa' ),
+			new Element( 'li', [], 'bbbbbbbbbb' ),
+			new Element( 'li', [], 'cccccccccc' ),
+			new Element( 'li', [], 'dddddddddd' ),
+			new Element( 'li', [], 'eeeeeeeeee' ),
+			new Element( 'li', [], 'ffffffffff' ),
+			new Element( 'li', [], 'gggggggggg' ),
+			new Element( 'li', [], 'hhhhhhhhhh' )
+		];
+
+		ul = new Element( 'ul', [], lis );
+		p = new Element( 'p', [], 'qwertyuiop' );
+
+		root.insertChildren( 0, [ ul, p, 'xyzxyz' ] );
+	} );
+
+	it( 'should be an instance of Range', () => {
+		let live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		live.detach();
+
+		expect( live ).to.be.instanceof( Range );
+	} );
+
+	it( 'should return instance of Range when cloned', () => {
+		let live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		let clone = live.clone();
+
+		expect( clone ).not.to.be.instanceof( LiveRange );
+
+		live.detach();
+	} );
+
+	it( 'should return instance of LiveRange when cloned with flag set to true', () => {
+		let live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		let clone = live.clone( true );
+
+		expect( clone ).to.be.instanceof( LiveRange );
+
+		live.detach();
+		clone.detach();
+	} );
+
+	it( 'should listen to a change event of the document that owns this range', () => {
+		sinon.spy( LiveRange.prototype, 'listenTo' );
+
+		let live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		live.detach();
+
+		expect( live.listenTo.calledWith( doc, 'change' ) ).to.be.true;
+
+		LiveRange.prototype.listenTo.restore();
+	} );
+
+	it( 'should stop listening when detached', () => {
+		sinon.spy( LiveRange.prototype, 'stopListening' );
+
+		let live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		live.detach();
+
+		expect( live.stopListening.called ).to.be.true;
+
+		LiveRange.prototype.stopListening.restore();
+	} );
+
+	it( 'createFromElement should return LiveRange', () => {
+		let range = LiveRange.createFromElement( p );
+		expect( range ).to.be.instanceof( LiveRange );
+		range.detach();
+	} );
+
+	it( 'createFromParentsAndOffsets should return LiveRange', () => {
+		let range = LiveRange.createFromParentsAndOffsets( root, 0, p, 2 );
+		expect( range ).to.be.instanceof( LiveRange );
+		range.detach();
+	} );
+
+	it( 'createFromPositionAndShift should return LiveRange', () => {
+		let range = LiveRange.createFromPositionAndShift( new Position( root, [ 0, 1 ] ), 4 );
+		expect( range ).to.be.instanceof( LiveRange );
+		range.detach();
+	} );
+
+	// Examples may be weird when you compare them with the tree structure generated at the beginning of tests.
+	// Since change event is fired _after_ operation is executed on tree model, you have to imagine that generated
+	// structure is representing what is _after_ operation is executed. So live LiveRange properties are describing
+	// virtual tree that is not existing anymore and event ranges are operating on the tree generated above.
+	describe( 'should get transformed if', () => {
+		let live;
+
+		beforeEach( () => {
+			live = new LiveRange( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 2, 2 ] ) );
+		} );
+
+		afterEach( () => {
+			live.detach();
+		} );
+
+		describe( 'insertion', () => {
+			it( 'is in the same parent as range start and before it', () => {
+				let insertRange = new Range( new Position( root, [ 0, 1, 0 ] ), new Position( root, [ 0, 1, 4 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 8 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 2 ] );
+			} );
+
+			it( 'is in the same parent as range end and before it', () => {
+				let insertRange = new Range( new Position( root, [ 0, 2, 0 ] ), new Position( root, [ 0, 2, 3 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 5 ] );
+			} );
+
+			it( 'is at a position before a node from range start path', () => {
+				let insertRange = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 2 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 3, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 4, 2 ] );
+			} );
+
+			it( 'is at a position before a node from range end path', () => {
+				let insertRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 3 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 3, 2 ] );
+			} );
+		} );
+
+		describe( 'range move', () => {
+			it( 'is to the same parent as range start and before it', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 0, 1, 0 ] ), new Position( root, [ 0, 1, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 8 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 2 ] );
+			} );
+
+			it( 'is to the same parent as range end and before it', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 0, 2, 0 ] ), new Position( root, [ 0, 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 6 ] );
+			} );
+
+			it( 'is to a position before a node from range start path', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 2 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 3, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 4, 2 ] );
+			} );
+
+			it( 'is to a position before a node from range end path', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 3 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 3, 2 ] );
+			} );
+
+			it( 'is from the same parent as range start and before it', () => {
+				let moveSource = new Position( root, [ 0, 1, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 3 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 1 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 2 ] );
+			} );
+
+			it( 'is from the same parent as range end and before it', () => {
+				let moveSource = new Position( root, [ 0, 2, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 2 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 0 ] );
+			} );
+
+			it( 'is from a position before a node from range start path', () => {
+				let moveSource = new Position( root, [ 0, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 1 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 0, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 1, 2 ] );
+			} );
+
+			it( 'intersects on live range left side', () => {
+				let moveSource = new Position( root, [ 0, 1, 2 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 2 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 2 ] );
+			} );
+
+			it( 'intersects on live range right side', () => {
+				let moveSource = new Position( root, [ 0, 2, 1 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 1 ] );
+			} );
+
+			it( 'intersects on live range left side and live range new start is touching moved range end', () => {
+				let moveSource = new Position( root, [ 0, 1, 0 ] );
+				let moveRange = new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 6 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 5 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 7, 2 ] );
+			} );
+
+			it( 'intersects on live range right side and live range new end is touching moved range start', () => {
+				live.end.offset = 12;
+
+				let moveSource = new Position( root, [ 0, 2, 10 ] );
+				let moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 5 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 3, 2 ] );
+			} );
+
+			it( 'is equal to live range', () => {
+				live.end.path = [ 0, 1, 7 ];
+
+				let moveSource = new Position( root, [ 0, 1, 4 ] );
+				let moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 3 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 3, 0 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 3, 3 ] );
+			} );
+
+			it( 'contains live range', () => {
+				live.end.path = [ 0, 1, 7 ];
+
+				let moveSource = new Position( root, [ 0, 1, 3 ] );
+				let moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 9 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.start.path ).to.deep.equal( [ 0, 3, 1 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 3, 4 ] );
+			} );
+		} );
+	} );
+
+	describe( 'should not get transformed if', () => {
+		let otherRoot;
+
+		before( () => {
+			otherRoot = doc.createRoot( 'otherRoot' );
+		} );
+
+		let live, clone;
+
+		beforeEach( () => {
+			live = new LiveRange( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 2, 2 ] ) );
+			clone = live.clone();
+		} );
+
+		afterEach( () => {
+			live.detach();
+		} );
+
+		describe( 'insertion', () => {
+			// Technically range will be expanded but the boundaries properties will stay the same.
+			// Start won't change because insertion is after it.
+			// End won't change because it is in different node.
+			it( 'is in the same parent as range start and after it', () => {
+				let insertRange = new Range( new Position( root, [ 0, 1, 7 ] ), new Position( root, [ 0, 1, 9 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is in the same parent as range end and after it', () => {
+				let insertRange = new Range( new Position( root, [ 0, 2, 7 ] ), new Position( root, [ 0, 2, 9 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is to a position after a node from range end path', () => {
+				let insertRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 4 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is in different root', () => {
+				let insertRange = new Range( new Position( otherRoot, [ 0, 0 ] ), new Position( otherRoot, [ 0, 2 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'range move', () => {
+			// Technically range will be expanded but the boundaries properties will stay the same.
+			// Start won't change because insertion is after it.
+			// End won't change because it is in different node.
+			it( 'is to the same parent as range start and after it', () => {
+				let moveSource = new Position( root, [ 4 ] );
+				let moveRange = new Range( new Position( root, [ 0, 1, 7 ] ), new Position( root, [ 0, 1, 9 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is to the same parent as range end and before it', () => {
+				let moveSource = new Position( root, [ 4 ] );
+				let moveRange = new Range( new Position( root, [ 0, 2, 3 ] ), new Position( root, [ 0, 2, 5 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is to a position after a node from range end path', () => {
+				let moveSource = new Position( root, [ 4 ] );
+				let moveRange = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 5 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			// Technically range will be shrunk but the boundaries properties will stay the same.
+			// Start won't change because deletion is after it.
+			// End won't change because it is in different node.
+			it( 'is from the same parent as range start and after it', () => {
+				let moveSource = new Position( root, [ 0, 1, 6 ] );
+				let moveRange = new Range( new Position( root, [ 4, 0 ] ), new Position( root, [ 4, 3 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is from the same parent as range end and after it', () => {
+				let moveSource = new Position( root, [ 0, 2, 4 ] );
+				let moveRange = new Range( new Position( root, [ 4, 0 ] ), new Position( root, [ 4, 2 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is from a position after a node from range end path', () => {
+				let moveSource = new Position( root, [ 0, 3 ] );
+				let moveRange = new Range( new Position( root, [ 5, 0 ] ), new Position( root, [ 5, 1 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+
+			it( 'is to different root', () => {
+				it( 'is to the same parent as range start and before it', () => {
+					let moveSource = new Position( root, [ 2 ] );
+					let moveRange = new Range( new Position( otherRoot, [ 0, 1, 0 ] ), new Position( otherRoot, [ 0, 1, 4 ] ) );
+
+					doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+					expect( live.isEqual( clone ) ).to.be.true;
+				} );
+			} );
+
+			it( 'is from different root', () => {
+				let moveSource = new Position( otherRoot, [ 0, 2, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 2 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.isEqual( clone ) ).to.be.true;
+			} );
+		} );
+	} );
+} );