Răsfoiți Sursa

Merge branch 'master' into t/675

Piotrek Koszuliński 9 ani în urmă
părinte
comite
281fb08cf6
34 a modificat fișierele cu 388 adăugiri și 164 ștergeri
  1. 3 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  2. 16 8
      packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js
  3. 20 6
      packages/ckeditor5-engine/src/model/selection.js
  4. 9 0
      packages/ckeditor5-engine/src/view/document.js
  5. 5 1
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  6. 9 0
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  7. 13 0
      packages/ckeditor5-engine/src/view/observer/observer.js
  8. 30 7
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  9. 22 8
      packages/ckeditor5-engine/src/view/selection.js
  10. 28 7
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  11. 4 0
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  12. 4 0
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  13. 20 0
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  14. 8 0
      packages/ckeditor5-engine/tests/dev-utils/view.js
  15. 7 0
      packages/ckeditor5-engine/tests/manual/tickets/629/1.html
  16. 23 0
      packages/ckeditor5-engine/tests/manual/tickets/629/1.js
  17. 16 0
      packages/ckeditor5-engine/tests/manual/tickets/629/1.md
  18. 6 1
      packages/ckeditor5-engine/tests/model/liveselection.js
  19. 10 2
      packages/ckeditor5-engine/tests/model/selection.js
  20. 19 31
      packages/ckeditor5-engine/tests/view/document/document.js
  21. 6 0
      packages/ckeditor5-engine/tests/view/document/integration.js
  22. 1 2
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  23. 4 2
      packages/ckeditor5-engine/tests/view/domconverter/domconverter.js
  24. 4 0
      packages/ckeditor5-engine/tests/view/observer/clickobserver.js
  25. 1 0
      packages/ckeditor5-engine/tests/view/observer/domeventdata.js
  26. 4 1
      packages/ckeditor5-engine/tests/view/observer/domeventobserver.js
  27. 4 0
      packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js
  28. 4 0
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  29. 4 0
      packages/ckeditor5-engine/tests/view/observer/keyobserver.js
  30. 1 0
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  31. 72 86
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  32. 2 0
      packages/ckeditor5-engine/tests/view/position.js
  33. 5 1
      packages/ckeditor5-engine/tests/view/selection.js
  34. 4 0
      packages/ckeditor5-engine/tests/view/treewalker.js

+ 3 - 1
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -138,9 +138,11 @@ export default class EditingController {
 	}
 
 	/**
-	 * Removes all event listeners attached by the EditingController.
+	 * Removes all event listeners attached to the `EditingController`. Destroys all objects created
+	 * by `EditingController` that need to be destroyed.
 	 */
 	destroy() {
+		this.view.destroy();
 		this._listenter.stopListening();
 	}
 }

+ 16 - 8
packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js

@@ -10,6 +10,8 @@
  * @namespace engine.conversion.viewSelectionToModel
  */
 
+import ModelSelection from '../model/selection.js';
+
 /**
  * Function factory, creates a callback function which converts a {@link engine.view.Selection view selection} taken
  * from the {@link engine.view.Document#selectionChange} event and sets in on the {@link engine.model.Document#selection model}.
@@ -26,15 +28,21 @@
  */
 export function convertSelectionChange( modelDocument, mapper ) {
 	return ( evt, data ) => {
-		modelDocument.enqueueChanges( () => {
-			const viewSelection = data.newSelection;
-			const ranges = [];
+		const viewSelection = data.newSelection;
+		const modelSelection = new ModelSelection();
+
+		const ranges = [];
+
+		for ( let viewRange of viewSelection.getRanges() ) {
+			ranges.push( mapper.toModelRange( viewRange ) );
+		}
 
-			for ( let viewRange of viewSelection.getRanges() ) {
-				ranges.push( mapper.toModelRange( viewRange ) );
-			}
+		modelSelection.setRanges( ranges, viewSelection.isBackward );
 
-			modelDocument.selection.setRanges( ranges, viewSelection.isBackward );
-		} );
+		if ( !modelSelection.isEqual( modelDocument.selection ) ) {
+			modelDocument.enqueueChanges( () => {
+				modelDocument.selection.setTo( modelSelection );
+			} );
+		}
 	};
 }

+ 20 - 6
packages/ckeditor5-engine/src/model/selection.js

@@ -125,25 +125,39 @@ export default class Selection {
 	}
 
 	/**
-	 * Checks whether, this selection is equal to given selection. Selections equal if they have the same ranges and directions.
+	 * Checks whether this selection is equal to given selection. Selections are equal if they have same directions,
+	 * same number of ranges and all ranges from one selection equal to a range from other selection.
 	 *
 	 * @param {engine.model.Selection} otherSelection Selection to compare with.
 	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
 	 */
 	isEqual( otherSelection ) {
-		const rangeCount = this.rangeCount;
+		if ( this.rangeCount != otherSelection.rangeCount ) {
+			return false;
+		} else if ( this.rangeCount === 0 ) {
+			return true;
+		}
 
-		if ( rangeCount != otherSelection.rangeCount ) {
+		if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
 			return false;
 		}
 
-		for ( let i = 0; i < this.rangeCount; i++ ) {
-			if ( !this._ranges[ i ].isEqual( otherSelection._ranges[ i ] ) ) {
+		for ( let thisRange of this._ranges ) {
+			let found = false;
+
+			for ( let otherRange of otherSelection._ranges ) {
+				if ( thisRange.isEqual( otherRange ) ) {
+					found = true;
+					break;
+				}
+			}
+
+			if ( !found ) {
 				return false;
 			}
 		}
 
-		return this.isBackward === otherSelection.isBackward;
+		return true;
 	}
 
 	/**

+ 9 - 0
packages/ckeditor5-engine/src/view/document.js

@@ -303,6 +303,15 @@ export default class Document {
 			observer.enable();
 		}
 	}
+
+	/**
+	 * Destroys all observers created by view `Document`.
+	 */
+	destroy() {
+		for ( let observer of this._observers.values() ) {
+			observer.destroy();
+		}
+	}
 }
 
 mix( Document, ObservableMixin );

+ 5 - 1
packages/ckeditor5-engine/src/view/observer/domeventobserver.js

@@ -56,7 +56,11 @@ export default class DomEventObserver extends Observer {
 		const types = typeof this.domEventType == 'string' ? [ this.domEventType ] : this.domEventType;
 
 		types.forEach( type => {
-			domElement.addEventListener( type, domEvent => this.isEnabled && this.onDomEvent( domEvent ) );
+			this.listenTo( domElement, type, ( eventInfo, domEvent ) => {
+				if ( this.isEnabled ) {
+					this.onDomEvent( domEvent );
+				}
+			} );
 		} );
 	}
 

+ 9 - 0
packages/ckeditor5-engine/src/view/observer/mutationobserver.js

@@ -112,6 +112,15 @@ export default class MutationObserver extends Observer {
 		this._mutationObserver.disconnect();
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		this._mutationObserver.disconnect();
+	}
+
 	/**
 	 * Handles mutations. Deduplicates, mark view elements to sync, fire event and call render.
 	 *

+ 13 - 0
packages/ckeditor5-engine/src/view/observer/observer.js

@@ -3,6 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
+import DomEmitterMixin from '../../../utils/dom/emittermixin.js';
+import mix from '../../../utils/mix.js';
+
 /**
  * Abstract base observer class. Observers are classes which observe changes on DOM elements, do the preliminary
  * processing and fire events on the {@link engine.view.Document} objects. Observers can also add features to the view,
@@ -59,6 +62,14 @@ export default class Observer {
 		this.isEnabled = false;
 	}
 
+	/**
+	 * Disables and destroys the observer, among others removes event listeners created by the observer.
+	 */
+	destroy() {
+		this.disable();
+		this.stopListening();
+	}
+
 	/**
 	 * Starts observing the given root element.
 	 *
@@ -67,3 +78,5 @@ export default class Observer {
 	 * @param {String} name The name of the root element.
 	 */
 }
+
+mix( Observer, DomEmitterMixin );

+ 30 - 7
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -3,9 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+/* global setInterval, clearInterval */
+
 import Observer from './observer.js';
 import MutationObserver from './mutationobserver.js';
-
 import log from '../../../utils/log.js';
 
 /**
@@ -68,6 +69,8 @@ export default class SelectionObserver extends Observer {
 		 */
 		this._documents = new WeakSet();
 
+		this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 2000 );
+
 		/**
 		 * Private property to store the last selection, to check if the code does not enter infinite loop.
 		 *
@@ -101,11 +104,22 @@ export default class SelectionObserver extends Observer {
 			return;
 		}
 
-		domDocument.addEventListener( 'selectionchange', () => this._handleSelectionChange( domDocument ) );
+		this.listenTo( domDocument, 'selectionchange', () => {
+			this._handleSelectionChange( domDocument );
+		} );
 
 		this._documents.add( domDocument );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		clearInterval( this._clearInfiniteLoopInterval );
+	}
+
 	/**
 	 * Selection change listener. {@link engine.view.observer.MutationObserver#flush Flush} mutations, check if
 	 * selection changes and fires {@link engine.view.Document#selectionChange} event.
@@ -151,10 +165,6 @@ export default class SelectionObserver extends Observer {
 			newSelection: newViewSelection,
 			domSelection: domSelection
 		} );
-
-		// If nothing changes on `selectionChange` event, at this point we have "dirty DOM" (changed) and de-synched
-		// view (which has not been changed). In order to "reset DOM" we render the view again.
-		this.document.render();
 	}
 
 	/**
@@ -179,12 +189,25 @@ export default class SelectionObserver extends Observer {
 			this._loopbackCounter = 0;
 		}
 
-		if ( this._loopbackCounter > 10 ) {
+		// This counter is reset every 2 seconds. 50 selection changes in 2 seconds is enough high number
+		// to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
+		if ( this._loopbackCounter > 50 ) {
 			return true;
 		}
 
 		return false;
 	}
+
+	/**
+	 * Clears `SelectionObserver` internal properties connected with preventing infinite loop.
+	 *
+	 * @protected
+	 */
+	_clearInfiniteLoop() {
+		this._lastSelection = null;
+		this._lastButOneSelection = null;
+		this._loopbackCounter = 0;
+	}
 }
 
 /**

+ 22 - 8
packages/ckeditor5-engine/src/view/selection.js

@@ -283,33 +283,47 @@ export default class Selection {
 	}
 
 	/**
-	 * Checks whether, this selection is equal to given selection. Selections equal if they have the same ranges and directions.
+	 * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
+	 * same number of ranges and all ranges from one selection equal to a range from other selection.
 	 *
 	 * @param {engine.view.Selection} otherSelection Selection to compare with.
 	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
 	 */
 	isEqual( otherSelection ) {
-		const rangeCount = this.rangeCount;
+		if ( this.isFake != otherSelection.isFake ) {
+			return false;
+		}
 
-		if ( rangeCount != otherSelection.rangeCount ) {
+		if ( this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel ) {
 			return false;
 		}
 
-		if ( this.isFake != otherSelection.isFake ) {
+		if ( this.rangeCount != otherSelection.rangeCount ) {
 			return false;
+		} else if ( this.rangeCount === 0 ) {
+			return true;
 		}
 
-		if ( this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel ) {
+		if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
 			return false;
 		}
 
-		for ( let i = 0; i < this.rangeCount; i++ ) {
-			if ( !this._ranges[ i ].isEqual( otherSelection._ranges[ i ] ) ) {
+		for ( let thisRange of this._ranges ) {
+			let found = false;
+
+			for ( let otherRange of otherSelection._ranges ) {
+				if ( thisRange.isEqual( otherRange ) ) {
+					found = true;
+					break;
+				}
+			}
+
+			if ( !found ) {
 				return false;
 			}
 		}
 
-		return this._lastRangeBackward === otherSelection._lastRangeBackward;
+		return true;
 	}
 
 	/**

+ 28 - 7
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -27,18 +27,16 @@ import { getData as getViewData } from 'ckeditor5/engine/dev-utils/view.js';
 
 describe( 'EditingController', () => {
 	describe( 'constructor()', () => {
-		let model, editing;
-
-		beforeEach( () => {
-			model = new ModelDocument();
-			editing = new EditingController( model );
-		} );
-
 		it( 'should create controller with properties', () => {
+			const model = new ModelDocument();
+			const editing = new EditingController( model );
+
 			expect( editing ).to.have.property( 'model' ).that.equals( model );
 			expect( editing ).to.have.property( 'view' ).that.is.instanceof( ViewDocument );
 			expect( editing ).to.have.property( 'mapper' ).that.is.instanceof( Mapper );
 			expect( editing ).to.have.property( 'modelToView' ).that.is.instanceof( ModelConversionDispatcher );
+
+			editing.destroy();
 		} );
 	} );
 
@@ -53,6 +51,10 @@ describe( 'EditingController', () => {
 			editing = new EditingController( model );
 		} );
 
+		afterEach( () => {
+			editing.destroy();
+		} );
+
 		it( 'should create root', () => {
 			const domRoot = createElement( document, 'div', null, createElement( document, 'p' ) );
 
@@ -127,6 +129,7 @@ describe( 'EditingController', () => {
 		after( () => {
 			document.body.removeChild( domRoot );
 			listener.stopListening();
+			editing.destroy();
 		} );
 
 		beforeEach( () => {
@@ -273,6 +276,24 @@ describe( 'EditingController', () => {
 			} );
 
 			expect( spy.called ).to.be.false;
+
+			editing.destroy();
+		} );
+
+		it( 'should destroy view', () => {
+			let model, editing;
+
+			model = new ModelDocument();
+			model.createRoot();
+			model.schema.registerItem( 'paragraph', '$block' );
+
+			editing = new EditingController( model );
+
+			const spy = sinon.spy( editing.view, 'destroy' );
+
+			editing.destroy();
+
+			expect( spy.called ).to.be.true;
 		} );
 	} );
 } );

+ 4 - 0
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -92,6 +92,10 @@ describe( 'Model converter builder', () => {
 		dispatcher.on( 'remove', remove() );
 	} );
 
+	afterEach( () => {
+		viewDoc.destroy();
+	} );
+
 	describe( 'model element to view element conversion', () => {
 		it( 'using passed view element name', () => {
 			buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );

+ 4 - 0
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -63,6 +63,10 @@ beforeEach( () => {
 	dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
 } );
 
+afterEach( () => {
+	viewDoc.destroy();
+} );
+
 describe( 'default converters', () => {
 	beforeEach( () => {
 		// Selection converters for selection attributes.

+ 20 - 0
packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js

@@ -40,6 +40,10 @@ describe( 'convertSelectionChange', () => {
 		convertSelection = convertSelectionChange( model, mapper );
 	} );
 
+	afterEach( () => {
+		view.destroy();
+	} );
+
 	it( 'should convert collapsed selection', () => {
 		const viewSelection = new ViewSelection();
 		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
@@ -106,4 +110,20 @@ describe( 'convertSelectionChange', () => {
 		expect( modelGetData( model ) ).to.equal( '<paragraph>f[o]o</paragraph><paragraph>b[a]r</paragraph>' );
 		expect( model.selection.isBackward ).to.true;
 	} );
+
+	it( 'should not enqueue changes if selection has not changed', () => {
+		const viewSelection = new ViewSelection();
+		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
+			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
+
+		convertSelection( null, { newSelection: viewSelection } );
+
+		const spy = sinon.spy();
+
+		model.on( 'changesDone', spy );
+
+		convertSelection( null, { newSelection: viewSelection } );
+
+		expect( spy.called ).to.be.false;
+	} );
 } );

+ 8 - 0
packages/ckeditor5-engine/tests/dev-utils/view.js

@@ -48,6 +48,8 @@ describe( 'view test utils', () => {
 				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+
+				viewDocument.destroy();
 			} );
 
 			it( 'should use stringify method with selection', () => {
@@ -68,6 +70,8 @@ describe( 'view test utils', () => {
 				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+
+				viewDocument.destroy();
 			} );
 
 			it( 'should throw an error when passing invalid document', () => {
@@ -92,6 +96,8 @@ describe( 'view test utils', () => {
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
+
+				viewDocument.destroy();
 			} );
 
 			it( 'should use parse method with selection', () => {
@@ -107,6 +113,8 @@ describe( 'view test utils', () => {
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
+
+				viewDocument.destroy();
 			} );
 
 			it( 'should throw an error when passing invalid document', () => {

+ 7 - 0
packages/ckeditor5-engine/tests/manual/tickets/629/1.html

@@ -0,0 +1,7 @@
+<head>
+	<link rel="stylesheet" href="%APPS_DIR%ckeditor/build/modules/amd/theme/ckeditor.css">
+</head>
+
+<div id="editor">
+	<p>Lorem ipsum dolor sit amet.</p>
+</div>

+ 23 - 0
packages/ckeditor5-engine/tests/manual/tickets/629/1.js

@@ -0,0 +1,23 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '/ckeditor5/editor-classic/classic.js';
+import Enter from '/ckeditor5/enter/enter.js';
+import Typing from '/ckeditor5/typing/typing.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+import Bold from '/ckeditor5/basic-styles/bold.js';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	features: [ Enter, Typing, Paragraph, Bold ],
+	toolbar: [ 'bold' ]
+} )
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 16 - 0
packages/ckeditor5-engine/tests/manual/tickets/629/1.md

@@ -0,0 +1,16 @@
+@bender-ui: collapsed
+@bender-tags: ticket, 629, iteration4
+
+### Selection infinite loop [#629](https://github.com/ckeditor/ckeditor5-engine/issues/629)
+
+Before testing open console.
+
+1. Select part of text.
+2. Click bold or press <kbd>ctrl + b</kbd>.
+3. There should be no errors or warnings in console.
+
+
+1. Put caret somewhere in text.
+2. Press <kbd>left arrow</kbd>, then <kbd>right arrow</kbd>.
+3. Repeat as fast as you can for 2-3 seconds.
+4. There should be no errors or warnings in console.

+ 6 - 1
packages/ckeditor5-engine/tests/model/liveselection.js

@@ -21,6 +21,8 @@ import count from 'ckeditor5/utils/count.js';
 import testUtils from 'tests/core/_utils/utils.js';
 import { wrapInDelta } from 'tests/engine/model/_utils/utils.js';
 
+import log from 'ckeditor5/utils/log.js';
+
 testUtils.createSinonSandbox();
 
 describe( 'LiveSelection', () => {
@@ -136,13 +138,16 @@ describe( 'LiveSelection', () => {
 		} );
 
 		it( 'should not add a range that is in graveyard', () => {
+			const spy = testUtils.sinon.stub( log, 'warn' );
+
 			selection.addRange( Range.createIn( doc.graveyard ) );
 
 			expect( selection._ranges.length ).to.equal( 0 );
+			expect( spy.calledOnce ).to.be.true;
 		} );
 
 		it( 'should refresh attributes', () => {
-			const spy = sinon.spy( selection, '_updateAttributes' );
+			const spy = testUtils.sinon.spy( selection, '_updateAttributes' );
 
 			selection.addRange( range );
 

+ 10 - 2
packages/ckeditor5-engine/tests/model/selection.js

@@ -666,21 +666,29 @@ describe( 'Selection', () => {
 			expect( selection.isEqual( otherSelection ) ).to.be.true;
 		} );
 
+		it( 'should return true if both selections have no ranges', () => {
+			const otherSelection = new Selection();
+
+			expect( selection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
 		it( 'should return false if ranges count does not equal', () => {
 			selection.addRange( range1 );
 			selection.addRange( range2 );
 
 			const otherSelection = new Selection();
-			otherSelection.addRange( range1 );
+			otherSelection.addRange( range2 );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
 
-		it( 'should return false if ranges do not equal', () => {
+		it( 'should return false if ranges (other than the last added range) do not equal', () => {
 			selection.addRange( range1 );
+			selection.addRange( range3 );
 
 			const otherSelection = new Selection();
 			otherSelection.addRange( range2 );
+			otherSelection.addRange( range3 );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );

+ 19 - 31
packages/ckeditor5-engine/tests/view/document/document.js

@@ -25,7 +25,7 @@ testUtils.createSinonSandbox();
 
 describe( 'Document', () => {
 	const DEFAULT_OBSERVERS_COUNT = 5;
-	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled, domRoot;
+	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled, domRoot, viewDocument;
 
 	before( () => {
 		domRoot = createElement( document, 'div', {
@@ -65,12 +65,16 @@ describe( 'Document', () => {
 				enabled++;
 			}
 		};
+
+		viewDocument = new Document();
+	} );
+
+	afterEach( () => {
+		viewDocument.destroy();
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should create Document with all properties', () => {
-			const viewDocument = new Document();
-
 			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
 			expect( count( viewDocument.roots ) ).to.equal( 0 );
 			expect( viewDocument ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
@@ -79,8 +83,6 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should add default observers', () => {
-			const viewDocument = new Document();
-
 			expect( count( viewDocument._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
 			expect( viewDocument.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
 			expect( viewDocument.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
@@ -96,7 +98,6 @@ describe( 'Document', () => {
 			const domDiv = document.createElement( 'div' );
 			domDiv.appendChild( domP );
 
-			const viewDocument = new Document();
 			const ret = viewDocument.createRoot( domDiv );
 
 			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
@@ -115,7 +116,10 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should call observe on each observer', () => {
-			const viewDocument = new Document( document.createElement( 'div' ) );
+			// The variable will be overwritten.
+			viewDocument.destroy();
+
+			viewDocument = new Document( document.createElement( 'div' ) );
 			viewDocument.renderer.render = sinon.spy();
 
 			const domDiv1 = document.createElement( 'div' );
@@ -135,8 +139,6 @@ describe( 'Document', () => {
 
 		it( 'should create "main" root by default', () => {
 			const domDiv = document.createElement( 'div' );
-
-			const viewDocument = new Document();
 			const ret = viewDocument.createRoot( domDiv );
 
 			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
@@ -152,8 +154,6 @@ describe( 'Document', () => {
 
 		it( 'should create root with given name', () => {
 			const domDiv = document.createElement( 'div' );
-
-			const viewDocument = new Document();
 			const ret = viewDocument.createRoot( domDiv, 'header' );
 
 			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
@@ -168,7 +168,6 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should create root without attaching DOM element', () => {
-			const viewDocument = new Document();
 			const ret = viewDocument.createRoot( 'div' );
 
 			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
@@ -180,8 +179,6 @@ describe( 'Document', () => {
 	describe( 'attachDomRoot', () => {
 		it( 'should create root without attach DOM element to the view element', () => {
 			const domDiv = document.createElement( 'div' );
-
-			const viewDocument = new Document();
 			const viewRoot = viewDocument.createRoot( 'div' );
 
 			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
@@ -201,8 +198,6 @@ describe( 'Document', () => {
 
 		it( 'should create root without attach DOM element to the view element with given name', () => {
 			const domH1 = document.createElement( 'h1' );
-
-			const viewDocument = new Document();
 			viewDocument.createRoot( 'DIV' );
 			const viewH1 = viewDocument.createRoot( 'h1', 'header' );
 
@@ -225,7 +220,6 @@ describe( 'Document', () => {
 
 	describe( 'getRoot', () => {
 		it( 'should return "main" root', () => {
-			const viewDocument = new Document();
 			viewDocument.createRoot( document.createElement( 'div' ) );
 
 			expect( count( viewDocument.roots ) ).to.equal( 1 );
@@ -234,7 +228,6 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should return named root', () => {
-			const viewDocument = new Document();
 			viewDocument.createRoot( document.createElement( 'h1' ), 'header' );
 
 			expect( count( viewDocument.roots ) ).to.equal( 1 );
@@ -244,13 +237,18 @@ describe( 'Document', () => {
 	} );
 
 	describe( 'addObserver', () => {
-		let viewDocument;
-
 		beforeEach( () => {
+			// The variable will be overwritten.
+			viewDocument.destroy();
+
 			viewDocument = new Document( document.createElement( 'div' ) );
 			viewDocument.renderer.render = sinon.spy();
 		} );
 
+		afterEach( () => {
+			viewDocument.destroy();
+		} );
+
 		it( 'should be instantiated and enabled on adding', () => {
 			const observerMock = viewDocument.addObserver( ObserverMock );
 
@@ -316,8 +314,6 @@ describe( 'Document', () => {
 
 	describe( 'getObserver', () => {
 		it( 'should return observer it it is added', () => {
-			const viewDocument = new Document();
-
 			const addedObserverMock = viewDocument.addObserver( ObserverMock );
 			const getObserverMock = viewDocument.getObserver( ObserverMock );
 
@@ -326,7 +322,6 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should return undefined if observer is not added', () => {
-			const viewDocument = new Document();
 			const getObserverMock = viewDocument.getObserver( ObserverMock );
 
 			expect( getObserverMock ).to.be.undefined;
@@ -335,8 +330,6 @@ describe( 'Document', () => {
 
 	describe( 'disableObservers', () => {
 		it( 'should disable observers', () => {
-			const viewDocument = new Document();
-
 			const addedObserverMock = viewDocument.addObserver( ObserverMock );
 
 			expect( addedObserverMock.enable.calledOnce ).to.be.true;
@@ -351,8 +344,6 @@ describe( 'Document', () => {
 
 	describe( 'enableObservers', () => {
 		it( 'should enable observers', () => {
-			const viewDocument = new Document();
-
 			const addedObserverMock = viewDocument.addObserver( ObserverMock );
 
 			viewDocument.disableObservers();
@@ -369,8 +360,6 @@ describe( 'Document', () => {
 
 	describe( 'isFocused', () => {
 		it( 'should change renderer.isFocused too', () => {
-			const viewDocument = new Document();
-
 			expect( viewDocument.isFocused ).to.equal( false );
 			expect( viewDocument.renderer.isFocused ).to.equal( false );
 
@@ -382,10 +371,9 @@ describe( 'Document', () => {
 	} );
 
 	describe( 'focus', () => {
-		let viewDocument, domEditable, viewEditable;
+		let domEditable, viewEditable;
 
 		beforeEach( () => {
-			viewDocument = new Document();
 			domEditable = document.createElement( 'div' );
 			domEditable.setAttribute( 'contenteditable', 'true' );
 			document.body.appendChild( domEditable );

+ 6 - 0
packages/ckeditor5-engine/tests/view/document/integration.js

@@ -25,6 +25,8 @@ describe( 'Document integration', () => {
 
 		expect( domDiv.childNodes.length ).to.equal( 1 );
 		expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
+
+		viewDocument.destroy();
 	} );
 
 	it( 'should render changes in the Document', () => {
@@ -38,6 +40,8 @@ describe( 'Document integration', () => {
 
 		expect( domDiv.childNodes.length ).to.equal( 1 );
 		expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
+
+		viewDocument.destroy();
 	} );
 
 	it( 'should render attribute changes', () => {
@@ -58,5 +62,7 @@ describe( 'Document integration', () => {
 
 		expect( domRoot.childNodes.length ).to.equal( 1 );
 		expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
+
+		viewDocument.destroy();
 	} );
 } );

+ 1 - 2
packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js

@@ -33,8 +33,7 @@ describe( 'Document', () => {
 	} );
 
 	afterEach( () => {
-		// There's no destroy() method yet, so let's at least kill the observers.
-		viewDocument.disableObservers();
+		viewDocument.destroy();
 
 		domRoot.parentElement.removeChild( domRoot );
 	} );

+ 4 - 2
packages/ckeditor5-engine/tests/view/domconverter/domconverter.js

@@ -33,10 +33,11 @@ describe( 'DomConverter', () => {
 	} );
 
 	describe( 'focus', () => {
-		let viewEditable, domEditable;
+		let viewEditable, domEditable, viewDocument;
 
 		beforeEach( () => {
-			viewEditable = new ViewEditable( new ViewDocument(), 'div' );
+			viewDocument = new ViewDocument();
+			viewEditable = new ViewEditable( viewDocument, 'div' );
 			domEditable = document.createElement( 'div' );
 			converter.bindElements( domEditable, viewEditable );
 			domEditable.setAttribute( 'contenteditable', 'true' );
@@ -45,6 +46,7 @@ describe( 'DomConverter', () => {
 
 		afterEach( () => {
 			document.body.removeChild( domEditable );
+			viewDocument.destroy();
 		} );
 
 		it( 'should call focus on corresponding DOM editable', () => {

+ 4 - 0
packages/ckeditor5-engine/tests/view/observer/clickobserver.js

@@ -17,6 +17,10 @@ describe( 'ClickObserver', () => {
 		observer = viewDocument.addObserver( ClickObserver );
 	} );
 
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
 	it( 'should define domEventType', () => {
 		expect( observer.domEventType ).to.equal( 'click' );
 	} );

+ 1 - 0
packages/ckeditor5-engine/tests/view/observer/domeventdata.js

@@ -35,6 +35,7 @@ describe( 'DomEventData', () => {
 
 	afterEach( () => {
 		domRoot.parentElement.removeChild( domRoot );
+		viewDocument.destroy();
 	} );
 
 	describe( 'constructor()', () => {

+ 4 - 1
packages/ckeditor5-engine/tests/view/observer/domeventobserver.js

@@ -41,6 +41,10 @@ describe( 'DomEventObserver', () => {
 		viewDocument = new ViewDocument();
 	} );
 
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'should create Observer with properties', () => {
 			const observer = new DomEventObserver( viewDocument );
@@ -107,7 +111,6 @@ describe( 'DomEventObserver', () => {
 	it( 'should fire event if observer is disabled and re-enabled', () => {
 		const domElement = document.createElement( 'p' );
 		const domEvent = new MouseEvent( 'click' );
-		const viewDocument = new ViewDocument();
 		const evtSpy = sinon.spy();
 
 		viewDocument.createRoot( domElement, 'root' );

+ 4 - 0
packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js

@@ -36,6 +36,10 @@ describe( 'FakeSelectionObserver', () => {
 		viewDocument.selection.setFake();
 	} );
 
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
 	it( 'should do nothing if selection is not fake', () => {
 		viewDocument.selection.setFake( false );
 

+ 4 - 0
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -18,6 +18,10 @@ describe( 'FocusObserver', () => {
 		observer = viewDocument.getObserver( FocusObserver );
 	} );
 
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
 	it( 'should define domEventType', () => {
 		expect( observer.domEventType ).to.deep.equal( [ 'focus', 'blur' ] );
 	} );

+ 4 - 0
packages/ckeditor5-engine/tests/view/observer/keyobserver.js

@@ -18,6 +18,10 @@ describe( 'KeyObserver', () => {
 		observer = viewDocument.getObserver( KeyObserver );
 	} );
 
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
 	it( 'should define domEventType', () => {
 		expect( observer.domEventType ).to.equal( 'keydown' );
 	} );

+ 1 - 0
packages/ckeditor5-engine/tests/view/observer/mutationobserver.js

@@ -41,6 +41,7 @@ describe( 'MutationObserver', () => {
 		mutationObserver.disable();
 
 		domRoot.parentElement.removeChild( domRoot );
+		viewDocument.destroy();
 	} );
 
 	it( 'should handle typing', () => {

+ 72 - 86
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals setTimeout, Range, document */
+/* globals setTimeout, document */
 /* bender-tags: view, browser-only */
 
 import ViewRange from 'ckeditor5/engine/view/range.js';
@@ -13,7 +13,6 @@ import ViewDocument from 'ckeditor5/engine/view/document.js';
 import SelectionObserver from 'ckeditor5/engine/view/observer/selectionobserver.js';
 import MutationObserver from 'ckeditor5/engine/view/observer/mutationobserver.js';
 
-import EmitterMixin from 'ckeditor5/utils/emittermixin.js';
 import log from 'ckeditor5/utils/log.js';
 
 import { parse } from 'ckeditor5/engine/dev-utils/view.js';
@@ -21,15 +20,13 @@ import { parse } from 'ckeditor5/engine/dev-utils/view.js';
 testUtils.createSinonSandbox();
 
 describe( 'SelectionObserver', () => {
-	let viewDocument, viewRoot, mutationObserver, selectionObserver, listenter, domRoot;
+	let viewDocument, viewRoot, mutationObserver, selectionObserver, domRoot;
 
-	before( () => {
+	beforeEach( ( done ) => {
 		domRoot = document.createElement( 'div' );
 		domRoot.innerHTML = `<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>`;
 		document.body.appendChild( domRoot );
 
-		listenter = Object.create( EmitterMixin );
-
 		viewDocument = new ViewDocument();
 		viewDocument.createRoot( document.getElementById( 'main' ) );
 
@@ -43,30 +40,26 @@ describe( 'SelectionObserver', () => {
 			'<container:p>yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</container:p>' ) );
 
 		viewDocument.render();
-	} );
-
-	after( () => {
-		domRoot.parentElement.removeChild( domRoot );
-	} );
 
-	beforeEach( ( done ) => {
 		viewDocument.selection.removeAllRanges();
 		document.getSelection().removeAllRanges();
 
 		viewDocument.isFocused = true;
 
-		viewDocument.getObserver( SelectionObserver ).enable();
+		selectionObserver.enable();
 
 		// Ensure selectionchange will not be fired.
 		setTimeout( () => done(), 100 );
 	} );
 
 	afterEach( () => {
-		listenter.stopListening();
+		domRoot.parentElement.removeChild( domRoot );
+
+		viewDocument.destroy();
 	} );
 
 	it( 'should fire selectionChange when it is the only change', ( done ) => {
-		listenter.listenTo( viewDocument, 'selectionChange', ( evt, data ) => {
+		viewDocument.on( 'selectionChange', ( evt, data ) => {
 			expect( data ).to.have.property( 'domSelection' ).that.equals( document.getSelection() );
 
 			expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( ViewSelection );
@@ -79,7 +72,7 @@ describe( 'SelectionObserver', () => {
 			const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
 
 			expect( newViewRange.start.parent ).to.equal( viewFoo );
-			expect( newViewRange.start.offset ).to.equal( 1 );
+			expect( newViewRange.start.offset ).to.equal( 2 );
 			expect( newViewRange.end.parent ).to.equal( viewFoo );
 			expect( newViewRange.end.offset ).to.equal( 2 );
 
@@ -93,7 +86,7 @@ describe( 'SelectionObserver', () => {
 		// Add second roots to ensure that listener is added once.
 		viewDocument.createRoot( document.getElementById( 'additional' ), 'additional' );
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
+		viewDocument.on( 'selectionChange', () => {
 			done();
 		} );
 
@@ -101,11 +94,11 @@ describe( 'SelectionObserver', () => {
 	} );
 
 	it( 'should not fire selectionChange on render', ( done ) => {
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
+		viewDocument.on( 'selectionChange', () => {
 			throw 'selectionChange on render';
 		} );
 
-		setTimeout( () => done(), 70 );
+		setTimeout( done, 70 );
 
 		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
 		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
@@ -115,11 +108,11 @@ describe( 'SelectionObserver', () => {
 	it( 'should not fired if observer is disabled', ( done ) => {
 		viewDocument.getObserver( SelectionObserver ).disable();
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
+		viewDocument.on( 'selectionChange', () => {
 			throw 'selectionChange on render';
 		} );
 
-		setTimeout( () => done(), 70 );
+		setTimeout( done, 70 );
 
 		changeDomSelection();
 	} );
@@ -127,28 +120,38 @@ describe( 'SelectionObserver', () => {
 	it( 'should not fired if there is no focus', ( done ) => {
 		viewDocument.isFocused = false;
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
+		// changeDomSelection() may focus the editable element (happens on Chrome)
+		// so cancel this because it sets the isFocused flag.
+		viewDocument.on( 'focus', ( evt ) => evt.stop(), { priority: 'highest' } );
+
+		viewDocument.on( 'selectionChange', () => {
+			// Validate the correctness of the test. May help tracking issue with this test.
+			expect( viewDocument.isFocused ).to.be.false;
+
 			throw 'selectionChange on render';
 		} );
 
-		setTimeout( () => done(), 70 );
+		setTimeout( done, 70 );
 
 		changeDomSelection();
 	} );
 
 	it( 'should warn and not enter infinite loop', ( done ) => {
-		let counter = 30;
+		// Reset infinite loop counters so other tests won't mess up with this test.
+		selectionObserver._clearInfiniteLoop();
+
+		let counter = 100;
 
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
 		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
+		viewDocument.on( 'selectionChange', () => {
 			counter--;
 
 			if ( counter > 0 ) {
 				setTimeout( changeDomSelection );
 			} else {
-				throw( 'Infinite loop!' );
+				throw 'Infinite loop!';
 			}
 		} );
 
@@ -168,86 +171,69 @@ describe( 'SelectionObserver', () => {
 		changeDomSelection();
 	} );
 
-	it( 'should not be treated as an infinite loop if the position is different', ( done ) => {
-		let counter = 30;
-
+	it( 'should not be treated as an infinite loop if selection is changed only few times', ( done ) => {
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
-		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
-
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
-			counter--;
-
-			if ( counter > 0 ) {
-				setTimeout( () => changeCollapsedDomSelection( counter ) );
-			} else {
-				done();
-			}
-		} );
-
-		changeCollapsedDomSelection( counter );
-	} );
 
-	it( 'should not be treated as an infinite loop if it is less then 3 times', ( done ) => {
-		let counter = 3;
+		// Reset infinite loop counters so other tests won't mess up with this test.
+		selectionObserver._clearInfiniteLoop();
 
-		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
 		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
-			counter--;
+		const spy = testUtils.sinon.spy( log, 'warn' );
 
-			if ( counter > 0 ) {
-				setTimeout( () => changeDomSelection() );
-			} else {
-				done();
-			}
-		} );
+		for ( let i = 0; i < 10; i++ ) {
+			changeDomSelection();
+		}
 
-		changeDomSelection();
+		setTimeout( () => {
+			expect( spy.called ).to.be.false;
+			done();
+		}, 400 );
 	} );
 
-	it( 'should call render after selection change which reset selection if it was not changed', ( done ) => {
-		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
-		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 0, viewBar, 1 ) );
+	it( 'should not be treated as an infinite loop if changes are not often', ( done ) => {
+		const clock = testUtils.sinon.useFakeTimers( 'setInterval', 'clearInterval' );
+		const spy = testUtils.sinon.spy( log, 'warn' );
 
-		listenter.listenTo( viewDocument, 'selectionChange', () => {
-			setTimeout( () => {
-				const domSelection = document.getSelection();
+		// We need to recreate SelectionObserver, so it will use mocked setInterval.
+		selectionObserver.disable();
+		selectionObserver.destroy();
+		viewDocument._observers.delete( SelectionObserver );
+		viewDocument.addObserver( SelectionObserver );
+
+		// Inf-loop kicks in after 50th time the selection is changed in 2s.
+		// We will test 30 times, tick sinon clock to clean counter and then test 30 times again.
+		// Note that `changeDomSelection` fires two events.
+		let changeCount = 15;
 
-				expect( domSelection.rangeCount ).to.equal( 1 );
+		for ( let i = 0; i < changeCount; i++ ) {
+			setTimeout( () => {
+				changeDomSelection();
+			}, i * 20 );
+		}
 
-				const domRange = domSelection.getRangeAt( 0 );
-				const domBar = document.getElementById( 'main' ).childNodes[ 1 ].childNodes[ 0 ];
+		setTimeout( () => {
+			// Move the clock by 2100ms which will trigger callback added to `setInterval` and reset the inf-loop counter.
+			clock.tick( 2100 );
 
-				expect( domRange.startContainer ).to.equal( domBar );
-				expect( domRange.startOffset ).to.equal( 0 );
-				expect( domRange.endContainer ).to.equal( domBar );
-				expect( domRange.endOffset ).to.equal( 1 );
+			for ( let i = 0; i < changeCount; i++ ) {
+				changeDomSelection();
+			}
 
+			setTimeout( () => {
+				expect( spy.called ).to.be.false;
+				clock.restore();
 				done();
-			} );
-		} );
-
-		changeDomSelection();
+			}, 200 );
+		}, 400 );
 	} );
 } );
 
-function changeCollapsedDomSelection( pos = 1 ) {
+function changeDomSelection() {
 	const domSelection = document.getSelection();
-	domSelection.removeAllRanges();
 	const domFoo = document.getElementById( 'main' ).childNodes[ 0 ].childNodes[ 0 ];
-	const domRange = new Range();
-	domRange.setStart( domFoo, pos );
-	domRange.setEnd( domFoo, pos );
-	domSelection.addRange( domRange );
-}
+	const offset = domSelection.anchorOffset;
 
-function changeDomSelection() {
-	const domSelection = document.getSelection();
 	domSelection.removeAllRanges();
-	const domFoo = document.getElementById( 'main' ).childNodes[ 0 ].childNodes[ 0 ];
-	const domRange = new Range();
-	domRange.setStart( domFoo, 1 );
-	domRange.setEnd( domFoo, 2 );
-	domSelection.addRange( domRange );
+	domSelection.collapse( domFoo, offset == 2 ? 3 : 2 );
 }

+ 2 - 0
packages/ckeditor5-engine/tests/view/position.js

@@ -463,6 +463,8 @@ describe( 'Position', () => {
 			const position = new Position( p, 0 );
 
 			expect( position.editableElement ).to.equal( editable );
+
+			document.destroy();
 		} );
 	} );
 } );

+ 5 - 1
packages/ckeditor5-engine/tests/view/selection.js

@@ -443,11 +443,13 @@ describe( 'Selection', () => {
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
 
-		it( 'should return false if ranges do not equal', () => {
+		it( 'should return false if ranges (other than the last added one) do not equal', () => {
 			selection.addRange( range1 );
+			selection.addRange( range3 );
 
 			const otherSelection = new Selection();
 			otherSelection.addRange( range2 );
+			otherSelection.addRange( range3 );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
@@ -706,6 +708,8 @@ describe( 'Selection', () => {
 			selection.addRange( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
 
 			expect( selection.editableElement ).to.equal( root );
+
+			viewDocument.destroy();
 		} );
 	} );
 

+ 4 - 0
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -53,6 +53,10 @@ describe( 'TreeWalker', () => {
 		rootEnding = new Position( root, 2 );
 	} );
 
+	afterEach( () => {
+		doc.destroy();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'should throw if neither boundaries nor starting position is set', () => {
 			expect( () => {