Przeglądaj źródła

Merge pull request #1233 from ckeditor/t/ckeditor5-core/110

BREAKING CHANGE: `view/Document#roots` and `model/Document#roots` now are `Collection` instances.
BREAKING CHANGE `view/Document#createRoot` is removed in favor of roots collections binding.
BREAKING CHANGE: `view/Model#hasRoot` is removed in favor of `view/Model#getRoot`.
BREAKING CHANGE: `EditingController#createRoot` is removed.
Piotr Jasiun 8 lat temu
rodzic
commit
92f3ac5e4e
35 zmienionych plików z 349 dodań i 352 usunięć
  1. 16 24
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  2. 1 1
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  3. 9 30
      packages/ckeditor5-engine/src/model/document.js
  4. 1 1
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  5. 1 1
      packages/ckeditor5-engine/src/model/position.js
  6. 24 56
      packages/ckeditor5-engine/src/view/document.js
  7. 13 1
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  8. 11 64
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  9. 5 1
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js
  10. 4 1
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  11. 4 1
      packages/ckeditor5-engine/tests/conversion/definition-based-converters.js
  12. 2 1
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  13. 4 1
      packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js
  14. 2 1
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  15. 6 12
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  16. 15 4
      packages/ckeditor5-engine/tests/dev-utils/view.js
  17. 40 23
      packages/ckeditor5-engine/tests/model/document/document.js
  18. 24 0
      packages/ckeditor5-engine/tests/view/_utils/createroot.js
  19. 40 100
      packages/ckeditor5-engine/tests/view/document/document.js
  20. 19 3
      packages/ckeditor5-engine/tests/view/document/integration.js
  21. 3 1
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  22. 3 1
      packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js
  23. 5 2
      packages/ckeditor5-engine/tests/view/manual/clickobserver.js
  24. 4 1
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  25. 13 6
      packages/ckeditor5-engine/tests/view/observer/domeventobserver.js
  26. 3 1
      packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js
  27. 5 2
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  28. 7 3
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  29. 5 2
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  30. 3 2
      packages/ckeditor5-engine/tests/view/placeholder.js
  31. 3 1
      packages/ckeditor5-engine/tests/view/renderer.js
  32. 12 0
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  33. 2 1
      packages/ckeditor5-engine/tests/view/selection.js
  34. 2 3
      packages/ckeditor5-engine/tests/view/treewalker.js
  35. 38 0
      packages/ckeditor5-engine/tests/view/utils-tests/createroot.js

+ 16 - 24
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -7,6 +7,7 @@
  * @module engine/controller/editingcontroller
  */
 
+import RootEditableElement from '../view/rooteditableelement';
 import ModelDiffer from '../model/differ';
 import ViewDocument from '../view/document';
 import Mapper from '../conversion/mapper';
@@ -141,33 +142,24 @@ export default class EditingController {
 		this.modelToView.on( 'selection', clearFakeSelection(), { priority: 'low' } );
 		this.modelToView.on( 'selection', convertRangeSelection(), { priority: 'low' } );
 		this.modelToView.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
-	}
 
-	/**
-	 * {@link module:engine/view/document~Document#createRoot Creates} a view root
-	 * and {@link module:engine/conversion/mapper~Mapper#bindElements binds}
-	 * the model root with view root and and view root with DOM element:
-	 *
-	 *		editing.createRoot( document.querySelector( div#editor ) );
-	 *
-	 * If the DOM element is not available at the time you want to create a view root, for instance it is iframe body
-	 * element, it is possible to create view element and bind the DOM element later:
-	 *
-	 *		editing.createRoot( 'body' );
-	 *		editing.view.attachDomRoot( iframe.contentDocument.body );
-	 *
-	 * @param {Element|String} domRoot DOM root element or the name of view root element if the DOM element will be
-	 * attached later.
-	 * @param {String} [name='main'] Root name.
-	 * @returns {module:engine/view/containerelement~ContainerElement} View root element.
-	 */
-	createRoot( domRoot, name = 'main' ) {
-		const viewRoot = this.view.createRoot( domRoot, name );
-		const modelRoot = this.model.document.getRoot( name );
+		// Binds {@link module:engine/view/document~Document#roots view roots collection} to
+		// {@link module:engine/model/document~Document#roots model roots collection} so creating
+		// model root automatically creates corresponding view root.
+		this.view.roots.bindTo( this.model.document.roots ).using( root => {
+			// $graveyard is a special root that has no reflection in the view.
+			if ( root.rootName == '$graveyard' ) {
+				return null;
+			}
 
-		this.mapper.bindElements( modelRoot, viewRoot );
+			const viewRoot = new RootEditableElement( root.name );
 
-		return viewRoot;
+			viewRoot.rootName = root.rootName;
+			viewRoot.document = this.view;
+			this.mapper.bindElements( root, viewRoot );
+
+			return viewRoot;
+		} );
 	}
 
 	/**

+ 1 - 1
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -679,7 +679,7 @@ class DebugPlugin extends Plugin {
 function dumpTrees( document, version ) {
 	let string = '';
 
-	for ( const root of document.roots.values() ) {
+	for ( const root of document.roots ) {
 		string += root.printTree() + '\n';
 	}
 

+ 9 - 30
packages/ckeditor5-engine/src/model/document.js

@@ -13,6 +13,7 @@ import RootElement from './rootelement';
 import History from './history';
 import DocumentSelection from './documentselection';
 import TreeWalker from './treewalker';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -76,9 +77,9 @@ export default class Document {
 		 * {@link #getRoot} to manipulate it.
 		 *
 		 * @readonly
-		 * @member {Map}
+		 * @member {module:utils/collection~Collection}
 		 */
-		this.roots = new Map();
+		this.roots = new Collection( { idProperty: 'rootName' } );
 
 		// Add events that will ensure selection correctness.
 		this.selection.on( 'change:range', () => {
@@ -155,7 +156,7 @@ export default class Document {
 	 * @returns {module:engine/model/rootelement~RootElement} Created root.
 	 */
 	createRoot( elementName = '$root', rootName = 'main' ) {
-		if ( this.roots.has( rootName ) ) {
+		if ( this.roots.get( rootName ) ) {
 			/**
 			 * Root with specified name already exists.
 			 *
@@ -170,7 +171,7 @@ export default class Document {
 		}
 
 		const root = new RootElement( this, elementName, rootName );
-		this.roots.set( rootName, root );
+		this.roots.add( root );
 
 		return root;
 	}
@@ -187,42 +188,20 @@ export default class Document {
 	 * Returns top-level root by its name.
 	 *
 	 * @param {String} [name='main'] Unique root name.
-	 * @returns {module:engine/model/rootelement~RootElement} Root registered under given name.
+	 * @returns {module:engine/model/rootelement~RootElement|null} Root registered under given name or null when
+	 * there is no root of given name.
 	 */
 	getRoot( name = 'main' ) {
-		if ( !this.roots.has( name ) ) {
-			/**
-			 * Root with specified name does not exist.
-			 *
-			 * @error model-document-getRoot-root-not-exist
-			 * @param {String} name
-			 */
-			throw new CKEditorError(
-				'model-document-getRoot-root-not-exist: Root with specified name does not exist.',
-				{ name }
-			);
-		}
-
 		return this.roots.get( name );
 	}
 
-	/**
-	 * Checks if root with given name is defined.
-	 *
-	 * @param {String} name Name of root to check.
-	 * @returns {Boolean}
-	 */
-	hasRoot( name ) {
-		return this.roots.has( name );
-	}
-
 	/**
 	 * Returns array with names of all roots (without the {@link #graveyard}) added to the document.
 	 *
 	 * @returns {Array.<String>} Roots names.
 	 */
 	getRootNames() {
-		return Array.from( this.roots.keys() ).filter( name => name != graveyardName );
+		return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
 	}
 
 	/**
@@ -301,7 +280,7 @@ export default class Document {
 	 * @returns {module:engine/model/rootelement~RootElement} The default root for this document.
 	 */
 	_getDefaultRoot() {
-		for ( const root of this.roots.values() ) {
+		for ( const root of this.roots ) {
 			if ( root !== this.graveyard ) {
 				return root;
 			}

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js

@@ -168,7 +168,7 @@ export default class RootAttributeOperation extends Operation {
 	 * @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
 	 */
 	static fromJSON( json, document ) {
-		if ( !document.hasRoot( json.root ) ) {
+		if ( !document.getRoot( json.root ) ) {
 			/**
 			 * Cannot create RootAttributeOperation for document. Root with specified name does not exist.
 			 *

+ 1 - 1
packages/ckeditor5-engine/src/model/position.js

@@ -764,7 +764,7 @@ export default class Position {
 			return new Position( doc.graveyard, json.path );
 		}
 
-		if ( !doc.hasRoot( json.root ) ) {
+		if ( !doc.getRoot( json.root ) ) {
 			/**
 			 * Cannot create position for document. Root with specified name does not exist.
 			 *

+ 24 - 56
packages/ckeditor5-engine/src/view/document.js

@@ -10,7 +10,6 @@
 import Selection from './selection';
 import Renderer from './renderer';
 import DomConverter from './domconverter';
-import RootEditableElement from './rooteditableelement';
 import { injectQuirksHandling } from './filler';
 import { injectUiElementHandling } from './uielement';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -19,6 +18,7 @@ import SelectionObserver from './observer/selectionobserver';
 import FocusObserver from './observer/focusobserver';
 import KeyObserver from './observer/keyobserver';
 import FakeSelectionObserver from './observer/fakeselectionobserver';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
@@ -74,12 +74,17 @@ export default class Document {
 		this.domConverter = new DomConverter();
 
 		/**
-		 * Roots of the view tree. Map of the {module:engine/view/element~Element view elements} with roots names as keys.
+		 * Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.
+		 *
+		 * View roots are created as a result of binding between {@link module:engine/view/document~Document#roots} and
+		 * {@link module:engine/model/document~Document#roots} and this is handled by
+		 * {@link module:engine/controller/editingcontroller~EditingController}, so to create view root we need to create
+		 * model root using {@link module:engine/model/document~Document#createRoot}.
 		 *
 		 * @readonly
-		 * @member {Map} module:engine/view/document~Document#roots
+		 * @member {Collection} module:engine/view/document~Document#roots
 		 */
-		this.roots = new Map();
+		this.roots = new Collection( { idProperty: 'rootName' } );
 
 		/**
 		 * Defines whether document is in read-only mode.
@@ -177,69 +182,31 @@ export default class Document {
 	}
 
 	/**
-	 * Creates a {@link module:engine/view/document~Document#roots view root element}.
-	 *
-	 * If the DOM element is passed as a first parameter it will be automatically
-	 * {@link module:engine/view/document~Document#attachDomRoot attached}:
-	 *
-	 *		document.createRoot( document.querySelector( 'div#editor' ) ); // Will call document.attachDomRoot.
-	 *
-	 * However, if the string is passed, then only the view element will be created and the DOM element have to be
-	 * attached separately:
-	 *
-	 *		document.createRoot( 'body' );
-	 *		document.attachDomRoot( document.querySelector( 'body#editor' ) );
-	 *
-	 * In both cases, {@link module:engine/view/rooteditableelement~RootEditableElement#rootName element name} is always
-	 * transformed to lower
-	 * case.
-	 *
-	 * @param {Element|String} domRoot DOM root element or the tag name of view root element if the DOM element will be
-	 * attached later.
-	 * @param {String} [name='main'] Name of the root.
-	 * @returns {module:engine/view/rooteditableelement~RootEditableElement} The created view root element.
-	 */
-	createRoot( domRoot, name = 'main' ) {
-		const rootTag = typeof domRoot == 'string' ? domRoot : domRoot.tagName;
-
-		const viewRoot = new RootEditableElement( rootTag.toLowerCase(), name );
-		viewRoot.document = this;
-
-		this.roots.set( name, viewRoot );
-
-		// Mark changed nodes in the renderer.
-		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
-		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
-		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
-
-		if ( this.domConverter.isElement( domRoot ) ) {
-			this.attachDomRoot( domRoot, name );
-		}
-
-		return viewRoot;
-	}
-
-	/**
-	 * Attaches DOM root element to the view element and enable all observers on that element. This method also
-	 * {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view what means that all child
-	 * nodes will be removed and replaced with content of the view root.
+	 * Attaches DOM root element to the view element and enable all observers on that element.
+	 * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
+	 * what means that all child nodes will be removed and replaced with content of the view root.
 	 *
-	 * Note that {@link module:engine/view/document~Document#createRoot} will call this method automatically if the DOM element is
-	 * passed to it.
+	 * This method also will change view element name as the same as tag name of given dom root.
+	 * Name is always transformed to lower case.
 	 *
-	 * @param {Element|String} domRoot DOM root element.
+	 * @param {Element} domRoot DOM root element.
 	 * @param {String} [name='main'] Name of the root.
 	 */
 	attachDomRoot( domRoot, name = 'main' ) {
 		const viewRoot = this.getRoot( name );
 
-		this.domRoots.set( name, domRoot );
+		// Set view root name the same as DOM root tag name.
+		viewRoot._name = domRoot.tagName.toLowerCase();
 
+		this.domRoots.set( name, domRoot );
 		this.domConverter.bindElements( domRoot, viewRoot );
-
 		this.renderer.markToSync( 'children', viewRoot );
 		this.renderer.domDocuments.add( domRoot.ownerDocument );
 
+		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
+		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
+		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
+
 		for ( const observer of this._observers.values() ) {
 			observer.observe( domRoot, name );
 		}
@@ -250,7 +217,8 @@ export default class Document {
 	 * specific "main" root is returned.
 	 *
 	 * @param {String} [name='main'] Name of the root.
-	 * @returns {module:engine/view/rooteditableelement~RootEditableElement} The view root element with the specified name.
+	 * @returns {module:engine/view/rooteditableelement~RootEditableElement|null} The view root element with the specified name
+	 * or null when there is no root of given name.
 	 */
 	getRoot( name = 'main' ) {
 		return this.roots.get( name );

+ 13 - 1
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -23,7 +23,6 @@ export default class RootEditableElement extends EditableElement {
 	/**
 	 * Creates root editable element.
 	 *
-	 * @param {module:engine/view/document~Document} document {@link module:engine/view/document~Document} that is an owner of the root.
 	 * @param {String} name Node name.
 	 */
 	constructor( name ) {
@@ -56,4 +55,17 @@ export default class RootEditableElement extends EditableElement {
 	set rootName( rootName ) {
 		this.setCustomProperty( rootNameSymbol, rootName );
 	}
+
+	/**
+	 * Overrides old element name and sets new one.
+	 * This is needed because view roots are created before they are attached to the DOM.
+	 * The name of the root element is temporary at this stage. It has to be changed when the
+	 * view root element is attached to the DOM element.
+	 *
+	 * @protected
+	 * @param {String} name The new name of element.
+	 */
+	set _name( name ) {
+		this.name = name;
+	}
 }

+ 11 - 64
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -20,8 +20,6 @@ import ModelPosition from '../../src/model/position';
 import ModelRange from '../../src/model/range';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-
 import { parse, getData as getModelData } from '../../src/dev-utils/model';
 import { getData as getViewData } from '../../src/dev-utils/view';
 
@@ -55,71 +53,18 @@ describe( 'EditingController', () => {
 
 			sinon.assert.calledOnce( spy );
 		} );
-	} );
-
-	describe( 'createRoot()', () => {
-		let model, modelRoot, editing;
-
-		beforeEach( () => {
-			model = new Model();
-			modelRoot = model.document.createRoot();
-			model.document.createRoot( '$root', 'header' );
-
-			editing = new EditingController( model );
-		} );
-
-		afterEach( () => {
-			editing.destroy();
-			model.markers.destroy();
-		} );
-
-		it( 'should create root', () => {
-			const domRoot = createElement( document, 'div', null, createElement( document, 'p' ) );
-
-			const viewRoot = editing.createRoot( domRoot );
-
-			expect( viewRoot ).to.equal( editing.view.getRoot() );
-			expect( domRoot ).to.equal( editing.view.getDomRoot() );
-
-			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
-			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
-
-			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );
-			expect( editing.mapper.toViewElement( modelRoot ) ).to.equal( viewRoot );
-		} );
-
-		it( 'should create root with given name', () => {
-			const domRoot = createElement( document, 'div', null, createElement( document, 'p' ) );
-
-			const viewRoot = editing.createRoot( domRoot, 'header' );
-
-			expect( viewRoot ).to.equal( editing.view.getRoot( 'header' ) );
-			expect( domRoot ).to.equal( editing.view.getDomRoot( 'header' ) );
-
-			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
-			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
-
-			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( model.document.getRoot( 'header' ) );
-			expect( editing.mapper.toViewElement( model.document.getRoot( 'header' ) ) ).to.equal( viewRoot );
-		} );
-
-		it( 'should be possible to attach DOM element later', () => {
-			const domRoot = createElement( document, 'div', null, createElement( document, 'p' ) );
-
-			const viewRoot = editing.createRoot( 'div' );
 
-			expect( viewRoot ).to.equal( editing.view.getRoot() );
-			expect( editing.view.getDomRoot() ).to.be.undefined;
+		it( 'should bind view roots to model roots', () => {
+			expect( model.document.roots ).to.length( 1 ); // $graveyard
+			expect( editing.view.roots ).to.length( 0 );
 
-			editing.view.attachDomRoot( domRoot );
-
-			expect( domRoot ).to.equal( editing.view.getDomRoot() );
+			const modelRoot = model.document.createRoot();
 
-			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
-			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
+			expect( model.document.roots ).to.length( 2 );
+			expect( editing.view.roots ).to.length( 1 );
+			expect( editing.view.getRoot().document ).to.equal( editing.view );
 
-			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );
-			expect( editing.mapper.toViewElement( modelRoot ) ).to.equal( viewRoot );
+			expect( editing.view.getRoot().name ).to.equal( modelRoot.name ).to.equal( '$root' );
 		} );
 	} );
 
@@ -138,7 +83,9 @@ describe( 'EditingController', () => {
 			domRoot.contentEditable = true;
 
 			document.body.appendChild( domRoot );
-			viewRoot = editing.createRoot( domRoot );
+
+			viewRoot = editing.view.getRoot();
+			editing.view.attachDomRoot( domRoot );
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			model.schema.register( 'div', { inheritAllFrom: '$block' } );

+ 5 - 1
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -40,7 +40,11 @@ describe( 'advanced-converters', () => {
 
 		const editing = new EditingController( model );
 
-		viewRoot = editing.createRoot( 'div' );
+		viewRoot = editing.view.getRoot();
+
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		viewRoot._name = 'div';
 
 		viewDispatcher = new ViewConversionDispatcher( model, { schema: { checkChild: () => true } } );
 		viewDispatcher.on( 'text', convertText() );

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

@@ -63,7 +63,10 @@ describe( 'Model converter builder', () => {
 		modelRoot = modelDoc.createRoot();
 
 		controller = new EditingController( model );
-		controller.createRoot( 'div' );
+
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		controller.view.getRoot()._name = 'div';
 
 		dispatcher = controller.modelToView;
 

+ 4 - 1
packages/ckeditor5-engine/tests/conversion/definition-based-converters.js

@@ -112,7 +112,10 @@ describe( 'definition-based-converters', () => {
 		modelRoot = modelDoc.createRoot();
 
 		controller = new EditingController( model );
-		controller.createRoot( 'div' );
+
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		controller.view.getRoot()._name = 'div';
 
 		viewRoot = controller.view.getRoot();
 		dispatcher = controller.modelToView;

+ 2 - 1
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -34,6 +34,7 @@ import {
 	removeHighlight
 } from '../../src/conversion/model-to-view-converters';
 
+import createViewRoot from '../view/_utils/createroot';
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
@@ -49,7 +50,7 @@ describe( 'model-selection-to-view-converters', () => {
 		model.schema.extend( '$text', { allowIn: '$root' } );
 
 		viewDoc = new ViewDocument();
-		viewRoot = viewDoc.createRoot( 'div' );
+		viewRoot = createViewRoot( viewDoc );
 		viewSelection = viewDoc.selection;
 
 		mapper = new Mapper();

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

@@ -38,9 +38,12 @@ describe( 'model-to-view-converters', () => {
 		modelRoot = modelDoc.createRoot();
 
 		controller = new EditingController( model );
-		controller.createRoot( 'div' );
 
 		viewRoot = controller.view.getRoot();
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		controller.view.getRoot()._name = 'div';
+
 		dispatcher = controller.modelToView;
 
 		dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );

+ 2 - 1
packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js

@@ -6,6 +6,7 @@
 import ViewDocument from '../../src/view/document';
 import ViewSelection from '../../src/view/selection';
 import ViewRange from '../../src/view/range';
+import createViewRoot from '../view/_utils/createroot';
 
 import Model from '../../src/model/model';
 
@@ -26,7 +27,7 @@ describe( 'convertSelectionChange', () => {
 		modelSetData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
 		view = new ViewDocument();
-		viewRoot = view.createRoot( 'div' );
+		viewRoot = createViewRoot( view, 'div', 'main' );
 
 		viewSetData( view, '<p>foo</p><p>bar</p>' );
 

+ 6 - 12
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -46,6 +46,7 @@ import ViewTextProxy from '../../src/view/textproxy';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewElement from '../../src/view/element';
 
+import createViewRoot from '../view/_utils/createroot';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 testUtils.createSinonSandbox();
@@ -104,14 +105,7 @@ describe( 'disableEngineDebug', () => {
 describe( 'debug tools', () => {
 	let DebugPlugin, log, error;
 
-	class TestEditor extends StandardEditor {
-		constructor( ...args ) {
-			super( ...args );
-
-			this.model.document.createRoot( 'main' );
-			this.editing.createRoot( this.element, 'main' );
-		}
-	}
+	class TestEditor extends StandardEditor {}
 
 	before( () => {
 		log = sinon.spy();
@@ -707,7 +701,7 @@ describe( 'debug tools', () => {
 
 		it( 'for view', () => {
 			const viewDoc = new ViewDocument();
-			const viewRoot = viewDoc.createRoot( 'div' );
+			const viewRoot = createViewRoot( viewDoc );
 
 			viewRoot.appendChildren( [
 				new ViewContainerElement( 'p', { foo: 'bar' }, [
@@ -849,14 +843,14 @@ describe( 'debug tools', () => {
 
 			viewDoc.log( 0 );
 			expectLog(
-				'<div></div>'
+				'<$root></$root>'
 			);
 
 			viewDoc.log( 2 );
 			expectLog(
-				'<div>' +
+				'<$root>' +
 				'\n\tfbar' +
-				'\n</div>'
+				'\n</$root>'
 			);
 
 			sinon.spy( modelDoc, 'log' );

+ 15 - 4
packages/ckeditor5-engine/tests/dev-utils/view.js

@@ -18,6 +18,7 @@ import Selection from '../../src/view/selection';
 import Range from '../../src/view/range';
 import Document from '../../src/view/document';
 import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor';
+import createViewRoot from '../view/_utils/createroot';
 
 describe( 'view test utils', () => {
 	describe( 'getData, setData', () => {
@@ -37,7 +38,7 @@ describe( 'view test utils', () => {
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
 				const viewDocument = new Document();
 				const options = { showType: false, showPriority: false, withoutSelection: true };
-				const root = viewDocument.createRoot( element );
+				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 
 				expect( getData( viewDocument, options ) ).to.equal( '<p></p>' );
@@ -57,7 +58,7 @@ describe( 'view test utils', () => {
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
 				const viewDocument = new Document();
 				const options = { showType: false, showPriority: false };
-				const root = viewDocument.createRoot( element );
+				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 
 				viewDocument.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
@@ -87,7 +88,7 @@ describe( 'view test utils', () => {
 				const data = 'foobar<b>baz</b>';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
-				viewDocument.createRoot( document.createElement( 'div' ) );
+				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
 				setData( viewDocument, data );
 
 				expect( getData( viewDocument ) ).to.equal( 'foobar<b>baz</b>' );
@@ -105,7 +106,7 @@ describe( 'view test utils', () => {
 				const data = '[<b>baz</b>]';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
-				viewDocument.createRoot( document.createElement( 'div' ) );
+				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
 				setData( viewDocument, data );
 
 				expect( getData( viewDocument ) ).to.equal( '[<b>baz</b>]' );
@@ -677,3 +678,13 @@ describe( 'view test utils', () => {
 		} );
 	} );
 } );
+
+function createAttachedRoot( viewDocument, element ) {
+	const root = createViewRoot( viewDocument );
+
+	// Set name of view root the same as dom root.
+	// This is a mock of attaching view root to dom root.
+	root._name = element.tagName.toLowerCase();
+
+	return root;
+}

+ 40 - 23
packages/ckeditor5-engine/tests/model/document/document.js

@@ -9,6 +9,7 @@ import RootElement from '../../../src/model/rootelement';
 import Batch from '../../../src/model/batch';
 import Delta from '../../../src/model/delta/delta';
 import Range from '../../../src/model/range';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
@@ -31,8 +32,8 @@ describe( 'Document', () => {
 			const doc = new Document( model );
 
 			expect( doc ).to.have.property( 'model' ).to.equal( model );
-			expect( doc ).to.have.property( 'roots' ).that.is.instanceof( Map );
-			expect( doc.roots.size ).to.equal( 1 );
+			expect( doc ).to.have.property( 'roots' ).that.is.instanceof( Collection );
+			expect( doc.roots.length ).to.equal( 1 );
 			expect( doc.graveyard ).to.be.instanceof( RootElement );
 			expect( doc.graveyard.maxOffset ).to.equal( 0 );
 			expect( count( doc.selection.getRanges() ) ).to.equal( 1 );
@@ -133,7 +134,7 @@ describe( 'Document', () => {
 		it( 'should create a new RootElement with default element and root names, add it to roots map and return it', () => {
 			const root = doc.createRoot();
 
-			expect( doc.roots.size ).to.equal( 2 );
+			expect( doc.roots.length ).to.equal( 2 );
 			expect( root ).to.be.instanceof( RootElement );
 			expect( root.maxOffset ).to.equal( 0 );
 			expect( root ).to.have.property( 'name', '$root' );
@@ -143,7 +144,7 @@ describe( 'Document', () => {
 		it( 'should create a new RootElement with custom element and root names, add it to roots map and return it', () => {
 			const root = doc.createRoot( 'customElementName', 'customRootName' );
 
-			expect( doc.roots.size ).to.equal( 2 );
+			expect( doc.roots.length ).to.equal( 2 );
 			expect( root ).to.be.instanceof( RootElement );
 			expect( root.maxOffset ).to.equal( 0 );
 			expect( root ).to.have.property( 'name', 'customElementName' );
@@ -162,31 +163,20 @@ describe( 'Document', () => {
 	} );
 
 	describe( 'getRoot()', () => {
-		it( 'should return a RootElement previously created with given name', () => {
-			const newRoot = doc.createRoot();
-			const getRoot = doc.getRoot();
+		it( 'should return a RootElement with default "main" name', () => {
+			const newRoot = doc.createRoot( 'main' );
 
-			expect( getRoot ).to.equal( newRoot );
+			expect( doc.getRoot() ).to.equal( newRoot );
 		} );
 
-		it( 'should throw an error when trying to get non-existent root', () => {
-			expect(
-				() => {
-					doc.getRoot( 'root' );
-				}
-			).to.throw( CKEditorError, /model-document-getRoot-root-not-exist/ );
-		} );
-	} );
+		it( 'should return a RootElement with custom name', () => {
+			const newRoot = doc.createRoot( 'custom', 'custom' );
 
-	describe( 'hasRoot()', () => {
-		it( 'should return true when Document has RootElement with given name', () => {
-			doc.createRoot();
-
-			expect( doc.hasRoot( 'main' ) ).to.be.true;
+			expect( doc.getRoot( 'custom' ) ).to.equal( newRoot );
 		} );
 
-		it( 'should return false when Document does not have RootElement with given name', () => {
-			expect( doc.hasRoot( 'noroot' ) ).to.be.false;
+		it( 'should return null when trying to get non-existent root', () => {
+			expect( doc.getRoot( 'not-existing' ) ).to.null;
 		} );
 	} );
 
@@ -486,6 +476,33 @@ describe( 'Document', () => {
 		} );
 	} );
 
+	describe( 'destroy()', () => {
+		it( 'should destroy selection instance', () => {
+			const spy = sinon.spy( doc.selection, 'destroy' );
+
+			doc.destroy();
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should stop listening to events', () => {
+			const spy = sinon.spy();
+
+			doc.listenTo( model, 'something', spy );
+
+			model.fire( 'something' );
+
+			sinon.assert.calledOnce( spy );
+
+			doc.destroy();
+
+			model.fire( 'something' );
+
+			// Still once.
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+
 	it( 'should be correctly converted to json', () => {
 		const serialized = jsonParseStringify( doc );
 

+ 24 - 0
packages/ckeditor5-engine/tests/view/_utils/createroot.js

@@ -0,0 +1,24 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import RootEditableElement from '../../../src/view/rooteditableelement';
+
+/**
+ * Creates view root element and sets it to {@link module:engine/view/document~Document#roots roots collection}.
+ *
+ * @param {module:engine/view/document~Document} doc View document.
+ * @param {String} name Root element name.
+ * @param {String} rootName Root name.
+ * @returns {module:engine/view/rooteditableelement~RootEditableElement} Root element.
+ */
+export default function createRoot( doc, name = 'div', rootName = 'main' ) {
+	const root = new RootEditableElement( name );
+
+	root.document = doc;
+	root.rootName = rootName;
+	doc.roots.add( root );
+
+	return root;
+}

+ 40 - 100
packages/ckeditor5-engine/tests/view/document/document.js

@@ -20,6 +20,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import createViewRoot from '../_utils/createroot';
 
 testUtils.createSinonSandbox();
 
@@ -88,29 +89,37 @@ describe( 'Document', () => {
 		} );
 	} );
 
-	describe( 'createRoot()', () => {
-		it( 'should create root', () => {
-			const domP = document.createElement( 'p' );
+	describe( 'attachDomRoot()', () => {
+		it( 'should attach DOM element to main view element', () => {
 			const domDiv = document.createElement( 'div' );
-			domDiv.appendChild( domP );
-
-			const ret = viewDocument.createRoot( domDiv );
+			const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
 
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
+			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
 
-			const domRoot = viewDocument.getDomRoot();
-			const viewRoot = viewDocument.getRoot();
+			viewDocument.attachDomRoot( domDiv );
 
-			expect( ret ).to.equal( viewRoot );
+			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
 
-			expect( domRoot ).to.equal( domDiv );
+			expect( viewDocument.getDomRoot() ).to.equal( domDiv );
 			expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
 
-			expect( viewRoot.name ).to.equal( 'div' );
 			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 		} );
 
+		it( 'should attach DOM element to custom view element', () => {
+			const domH1 = document.createElement( 'h1' );
+			const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
+
+			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
+
+			viewDocument.attachDomRoot( domH1, 'header' );
+
+			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
+			expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
+			expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
+			expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;
+		} );
+
 		it( 'should call observe on each observer', () => {
 			// The variable will be overwritten.
 			viewDocument.destroy();
@@ -127,96 +136,17 @@ describe( 'Document', () => {
 			const observerMock = viewDocument.addObserver( ObserverMock );
 			const observerMockGlobalCount = viewDocument.addObserver( ObserverMockGlobalCount );
 
-			viewDocument.createRoot( document.createElement( 'div' ), 'root1' );
+			createViewRoot( viewDocument, 'div', 'root1' );
+			viewDocument.attachDomRoot( document.createElement( 'div' ), 'root1' );
 
 			sinon.assert.calledOnce( observerMock.observe );
 			sinon.assert.calledOnce( observerMockGlobalCount.observe );
 		} );
-
-		it( 'should create "main" root by default', () => {
-			const domDiv = document.createElement( 'div' );
-			const ret = viewDocument.createRoot( domDiv );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-
-			const domRoot = viewDocument.domRoots.get( 'main' );
-			const viewRoot = viewDocument.roots.get( 'main' );
-
-			expect( ret ).to.equal( viewRoot );
-
-			expect( domRoot ).to.equal( domDiv );
-		} );
-
-		it( 'should create root with given name', () => {
-			const domDiv = document.createElement( 'div' );
-			const ret = viewDocument.createRoot( domDiv, 'header' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-
-			const domRoot = viewDocument.domRoots.get( 'header' );
-			const viewRoot = viewDocument.roots.get( 'header' );
-
-			expect( ret ).to.equal( viewRoot );
-
-			expect( domRoot ).to.equal( domDiv );
-		} );
-
-		it( 'should create root without attaching DOM element', () => {
-			const ret = viewDocument.createRoot( 'div' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-			expect( ret ).to.equal( viewDocument.getRoot() );
-		} );
-	} );
-
-	describe( 'attachDomRoot()', () => {
-		it( 'should create root without attach DOM element to the view element', () => {
-			const domDiv = document.createElement( 'div' );
-			const viewRoot = viewDocument.createRoot( 'div' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-			expect( viewRoot ).to.equal( viewDocument.getRoot() );
-
-			viewDocument.attachDomRoot( domDiv );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-
-			expect( viewDocument.getDomRoot() ).to.equal( domDiv );
-			expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
-
-			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
-		} );
-
-		it( 'should create root without attach DOM element to the view element with given name', () => {
-			const domH1 = document.createElement( 'h1' );
-			viewDocument.createRoot( 'DIV' );
-			const viewH1 = viewDocument.createRoot( 'h1', 'header' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-			expect( count( viewDocument.roots ) ).to.equal( 2 );
-			expect( viewH1 ).to.equal( viewDocument.getRoot( 'header' ) );
-
-			viewDocument.attachDomRoot( domH1, 'header' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( count( viewDocument.roots ) ).to.equal( 2 );
-
-			expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
-			expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
-
-			expect( viewDocument.getRoot().name ).to.equal( 'div' );
-			expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;
-		} );
 	} );
 
 	describe( 'getRoot()', () => {
 		it( 'should return "main" root', () => {
-			viewDocument.createRoot( document.createElement( 'div' ) );
+			createViewRoot( viewDocument, 'div', 'main' );
 
 			expect( count( viewDocument.roots ) ).to.equal( 1 );
 
@@ -224,12 +154,16 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should return named root', () => {
-			viewDocument.createRoot( document.createElement( 'h1' ), 'header' );
+			createViewRoot( viewDocument, 'h1', 'header' );
 
 			expect( count( viewDocument.roots ) ).to.equal( 1 );
 
 			expect( viewDocument.getRoot( 'header' ) ).to.equal( viewDocument.roots.get( 'header' ) );
 		} );
+
+		it( 'should return null when trying to get non-existent root', () => {
+			expect( viewDocument.getRoot( 'not-existing' ) ).to.null;
+		} );
 	} );
 
 	describe( 'addObserver()', () => {
@@ -299,8 +233,11 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should call observe on each root', () => {
-			viewDocument.createRoot( document.createElement( 'div' ), 'root1' );
-			viewDocument.createRoot( document.createElement( 'div' ), 'root2' );
+			createViewRoot( viewDocument, 'div', 'roo1' );
+			createViewRoot( viewDocument, 'div', 'roo2' );
+
+			viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo1' );
+			viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo2' );
 
 			const observerMock = viewDocument.addObserver( ObserverMock );
 
@@ -338,10 +275,12 @@ describe( 'Document', () => {
 		} );
 
 		it( 'scrolls to the first range in selection with an offset', () => {
+			const root = createViewRoot( viewDocument, 'div', 'main' );
 			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
-			const root = viewDocument.createRoot( domRoot );
 			const range = ViewRange.createIn( root );
 
+			viewDocument.attachDomRoot( domRoot );
+
 			// Make sure the window will have to scroll to the domRoot.
 			Object.assign( domRoot.style, {
 				position: 'absolute',
@@ -405,7 +344,8 @@ describe( 'Document', () => {
 			domEditable = document.createElement( 'div' );
 			domEditable.setAttribute( 'contenteditable', 'true' );
 			document.body.appendChild( domEditable );
-			viewEditable = viewDocument.createRoot( domEditable );
+			viewEditable = createViewRoot( viewDocument, 'div', 'main' );
+			viewDocument.attachDomRoot( domEditable );
 			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
 		} );
 

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

@@ -5,6 +5,7 @@
 
 /* globals document */
 
+import RootEditableElement from '../../../src/view/rooteditableelement';
 import Document from '../../../src/view/document';
 import ViewElement from '../../../src/view/element';
 import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
@@ -19,7 +20,9 @@ describe( 'Document integration', () => {
 		] );
 
 		const viewDocument = new Document();
-		viewDocument.createRoot( domDiv );
+
+		createRoot( 'div', 'main', viewDocument );
+		viewDocument.attachDomRoot( domDiv );
 		viewDocument.render();
 
 		expect( domDiv.childNodes.length ).to.equal( 1 );
@@ -32,7 +35,8 @@ describe( 'Document integration', () => {
 		const domDiv = document.createElement( 'div' );
 
 		const viewDocument = new Document();
-		viewDocument.createRoot( domDiv );
+		createRoot( 'div', 'main', viewDocument );
+		viewDocument.attachDomRoot( domDiv );
 
 		viewDocument.getRoot().appendChildren( new ViewElement( 'p' ) );
 		viewDocument.render();
@@ -47,7 +51,9 @@ describe( 'Document integration', () => {
 		const domRoot = document.createElement( 'div' );
 
 		const viewDocument = new Document();
-		const viewRoot = viewDocument.createRoot( domRoot );
+		const viewRoot = createRoot( 'div', 'main', viewDocument );
+
+		viewDocument.attachDomRoot( domRoot );
 
 		const viewP = new ViewElement( 'p', { class: 'foo' } );
 		viewRoot.appendChildren( viewP );
@@ -65,3 +71,13 @@ describe( 'Document integration', () => {
 		viewDocument.destroy();
 	} );
 } );
+
+function createRoot( name, rootName, viewDoc ) {
+	const viewRoot = new RootEditableElement( name );
+
+	viewRoot.rootName = rootName;
+	viewRoot.document = viewDoc;
+	viewDoc.roots.add( viewRoot );
+
+	return viewRoot;
+}

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

@@ -9,6 +9,7 @@ import ViewRange from '../../../src/view/range';
 import ViewDocument from '../../../src/view/document';
 import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '../../../src/view/filler';
 
+import createViewRoot from '../_utils/createroot';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
@@ -24,7 +25,8 @@ describe( 'Document', () => {
 		document.body.appendChild( domRoot );
 
 		viewDocument = new ViewDocument();
-		viewDocument.createRoot( domRoot );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domRoot );
 
 		document.getSelection().removeAllRanges();
 

+ 3 - 1
packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js

@@ -13,6 +13,7 @@ import ViewText from '../../../src/view/text';
 import ViewRange from '../../../src/view/range';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+import createViewRoot from '../_utils/createroot';
 import { setData as setViewData } from '../../../src/dev-utils/view';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
@@ -35,7 +36,8 @@ describe( 'Document', () => {
 		document.body.appendChild( domRoot );
 
 		viewDocument = new ViewDocument();
-		viewRoot = viewDocument.createRoot( domRoot );
+		viewRoot = createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domRoot );
 
 		domSelection = document.getSelection();
 		domSelection.removeAllRanges();

+ 5 - 2
packages/ckeditor5-engine/tests/view/manual/clickobserver.js

@@ -7,6 +7,7 @@
 
 import Document from '../../../src/view/document';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
+import createViewRoot from '../_utils/createroot';
 
 const viewDocument = new Document();
 
@@ -45,8 +46,10 @@ document.getElementById( 'disable1' ).addEventListener( 'click', () => observer1
 // Random order.
 viewDocument.addObserver( ClickObserver1 );
 
-viewDocument.createRoot( document.getElementById( 'clickerA' ), 'clickerA' );
+createViewRoot( viewDocument, 'div', 'clickerA' );
+viewDocument.attachDomRoot( document.getElementById( 'clickerA' ), 'clickerA' );
 
 viewDocument.addObserver( ClickObserver2 );
 
-viewDocument.createRoot( document.getElementById( 'clickerB' ), 'clickerB' );
+createViewRoot( viewDocument, 'div', 'clickerB' );
+viewDocument.attachDomRoot( document.getElementById( 'clickerB' ), 'clickerB' );

+ 4 - 1
packages/ckeditor5-engine/tests/view/manual/fakeselection.js

@@ -8,13 +8,16 @@
 import ViewDocument from '../../../src/view/document';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import ViewRange from '../../../src/view/range';
+import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
 const viewDocument = new ViewDocument();
 const domEditable = document.getElementById( 'editor' );
-const viewRoot = viewDocument.createRoot( domEditable );
+const viewRoot = createViewRoot();
 let viewStrong;
 
+viewDocument.attachDomRoot( domEditable );
+
 // Add mouseup oberver.
 viewDocument.addObserver( class extends DomEventObserver {
 	get domEventType() {

+ 13 - 6
packages/ckeditor5-engine/tests/view/observer/domeventobserver.js

@@ -9,6 +9,7 @@ import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import Observer from '../../../src/view/observer/observer';
 import ViewDocument from '../../../src/view/document';
 import UIElement from '../../../src/view/uielement';
+import createViewRoot from '../_utils/createroot';
 
 class ClickObserver extends DomEventObserver {
 	constructor( document ) {
@@ -66,7 +67,8 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const evtSpy = sinon.spy();
 
-		viewDocument.createRoot( domElement, 'root' );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domElement );
 		viewDocument.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
@@ -88,7 +90,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy1 = sinon.spy();
 		const evtSpy2 = sinon.spy();
 
-		viewDocument.createRoot( domElement, 'root' );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domElement );
 		viewDocument.addObserver( MultiObserver );
 		viewDocument.on( 'evt1', evtSpy1 );
 		viewDocument.on( 'evt2', evtSpy2 );
@@ -105,7 +108,8 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const evtSpy = sinon.spy();
 
-		viewDocument.createRoot( domElement, 'root' );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domElement );
 		const testObserver = viewDocument.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
@@ -121,7 +125,8 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const evtSpy = sinon.spy();
 
-		viewDocument.createRoot( domElement, 'root' );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domElement );
 		const testObserver = viewDocument.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
@@ -143,7 +148,8 @@ describe( 'DomEventObserver', () => {
 		const childDomElement = document.createElement( 'p' );
 		const domEvent = new MouseEvent( 'click' );
 		domElement.appendChild( childDomElement );
-		viewDocument.createRoot( domElement, 'root' );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domElement );
 		viewDocument.addObserver( ClickCapturingObserver );
 
 		viewDocument.on( 'click', ( evt, domEventData ) => {
@@ -168,7 +174,8 @@ describe( 'DomEventObserver', () => {
 
 		beforeEach( () => {
 			domRoot = document.createElement( 'div' );
-			const viewRoot = viewDocument.createRoot( domRoot, 'root' );
+			const viewRoot = createViewRoot( viewDocument );
+			viewDocument.attachDomRoot( domRoot );
 			uiElement = new MyUIElement( 'p' );
 			viewRoot.appendChildren( uiElement );
 			viewDocument.render();

+ 3 - 1
packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js

@@ -9,6 +9,7 @@ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
 import ViewDocument from '../../../src/view/document';
 import DomEventData from '../../../src/view/observer/domeventdata';
+import createViewRoot from '../_utils/createroot';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData, stringify } from '../../../src/dev-utils/view';
 
@@ -28,7 +29,8 @@ describe( 'FakeSelectionObserver', () => {
 
 	beforeEach( () => {
 		viewDocument = new ViewDocument();
-		root = viewDocument.createRoot( domRoot );
+		root = createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domRoot );
 		observer = viewDocument.getObserver( FakeSelectionObserver );
 		viewDocument.selection.setFake();
 	} );

+ 5 - 2
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -7,6 +7,7 @@
 import FocusObserver from '../../../src/view/observer/focusobserver';
 import ViewDocument from '../../../src/view/document';
 import ViewRange from '../../../src/view/range';
+import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
 describe( 'FocusObserver', () => {
@@ -72,7 +73,8 @@ describe( 'FocusObserver', () => {
 			domMain = document.createElement( 'div' );
 			domHeader = document.createElement( 'h1' );
 
-			viewMain = viewDocument.createRoot( domMain );
+			viewMain = createViewRoot( viewDocument );
+			viewDocument.attachDomRoot( domMain );
 		} );
 
 		it( 'should set isFocused to true on focus', () => {
@@ -149,7 +151,8 @@ describe( 'FocusObserver', () => {
 			document.body.appendChild( domRoot );
 
 			viewDocument = new ViewDocument();
-			viewDocument.createRoot( domRoot );
+			createViewRoot( viewDocument );
+			viewDocument.attachDomRoot( domRoot );
 
 			observer = viewDocument.getObserver( FocusObserver );
 		} );

+ 7 - 3
packages/ckeditor5-engine/tests/view/observer/mutationobserver.js

@@ -8,6 +8,7 @@
 import ViewDocument from '../../../src/view/document';
 import MutationObserver from '../../../src/view/observer/mutationobserver';
 import UIElement from '../../../src/view/uielement';
+import createViewRoot from '../_utils/createroot';
 import { parse } from '../../../src/dev-utils/view';
 
 describe( 'MutationObserver', () => {
@@ -22,7 +23,8 @@ describe( 'MutationObserver', () => {
 		domEditor = document.getElementById( 'main' );
 		lastMutations = null;
 
-		viewDocument.createRoot( domEditor );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domEditor );
 		viewDocument.selection.removeAllRanges();
 		document.getSelection().removeAllRanges();
 
@@ -62,7 +64,8 @@ describe( 'MutationObserver', () => {
 	it( 'should not observe if disabled', () => {
 		const additional = document.getElementById( 'additional' );
 		mutationObserver.disable();
-		viewDocument.createRoot( additional, 'additional' );
+		createViewRoot( viewDocument, 'div', 'additional' );
+		viewDocument.attachDomRoot( additional, 'additional' );
 
 		additional.textContent = 'foobar';
 		mutationObserver.flush();
@@ -196,7 +199,8 @@ describe( 'MutationObserver', () => {
 		const domAdditionalEditor = document.getElementById( 'additional' );
 
 		// Prepare AdditionalEditor
-		viewDocument.createRoot( domAdditionalEditor, 'additional' );
+		createViewRoot( viewDocument, 'div', 'additional' );
+		viewDocument.attachDomRoot( domAdditionalEditor, 'additional' );
 
 		viewDocument.getRoot( 'additional' ).appendChildren(
 			parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );

+ 5 - 2
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -12,6 +12,7 @@ import ViewDocument from '../../../src/view/document';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
 import FocusObserver from '../../../src/view/observer/focusobserver';
 import log from '@ckeditor/ckeditor5-utils/src/log';
+import createViewRoot from '../_utils/createroot';
 import { parse } from '../../../src/dev-utils/view';
 
 testUtils.createSinonSandbox();
@@ -27,7 +28,8 @@ describe( 'SelectionObserver', () => {
 		domDocument.body.appendChild( domRoot );
 
 		viewDocument = new ViewDocument();
-		viewDocument.createRoot( domMain );
+		createViewRoot( viewDocument );
+		viewDocument.attachDomRoot( domMain );
 
 		selectionObserver = viewDocument.getObserver( SelectionObserver );
 
@@ -83,7 +85,8 @@ describe( 'SelectionObserver', () => {
 
 	it( 'should add only one listener to one document', done => {
 		// Add second roots to ensure that listener is added once.
-		viewDocument.createRoot( domDocument.getElementById( 'additional' ), 'additional' );
+		createViewRoot( viewDocument, 'div', 'additional' );
+		viewDocument.attachDomRoot( domDocument.getElementById( 'additional' ), 'additional' );
 
 		viewDocument.on( 'selectionChange', () => {
 			done();

+ 3 - 2
packages/ckeditor5-engine/tests/view/placeholder.js

@@ -4,6 +4,7 @@
  */
 
 import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder';
+import createViewRoot from './_utils/createroot';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewDocument from '../../src/view/document';
 import ViewRange from '../../src/view/range';
@@ -14,7 +15,7 @@ describe( 'placeholder', () => {
 
 	beforeEach( () => {
 		viewDocument = new ViewDocument();
-		viewRoot = viewDocument.createRoot( 'main' );
+		viewRoot = createViewRoot( viewDocument );
 		viewDocument.isFocused = true;
 	} );
 
@@ -131,7 +132,7 @@ describe( 'placeholder', () => {
 			const element = viewRoot.getChild( 0 );
 			const secondDocument = new ViewDocument();
 			secondDocument.isFocused = true;
-			const secondRoot = secondDocument.createRoot( 'main' );
+			const secondRoot = createViewRoot( secondDocument );
 			setData( secondDocument, '<div></div><div>{another div}</div>' );
 			const secondElement = secondRoot.getChild( 0 );
 

+ 3 - 1
packages/ckeditor5-engine/tests/view/renderer.js

@@ -19,6 +19,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { parse, setData as setViewData, getData as getViewData } from '../../src/dev-utils/view';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '../../src/view/filler';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import createViewRoot from './_utils/createroot';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import { unwrap, insert, remove } from '../../src/view/writer';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
@@ -1880,7 +1881,8 @@ describe( 'Renderer', () => {
 			viewDoc = new ViewDocument();
 			domRoot = document.createElement( 'div' );
 			document.body.appendChild( domRoot );
-			viewRoot = viewDoc.createRoot( domRoot );
+			viewRoot = createViewRoot( viewDoc );
+			viewDoc.attachDomRoot( domRoot );
 			converter = viewDoc.domConverter;
 		} );
 

+ 12 - 0
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -69,6 +69,18 @@ describe( 'RootEditableElement', () => {
 		} );
 	} );
 
+	describe( '_name', () => {
+		it( 'should set new name to element', () => {
+			const el = new RootEditableElement( '$root' );
+
+			expect( el.name ).to.equal( '$root' );
+
+			el._name = 'div';
+
+			expect( el.name ).to.equal( 'div' );
+		} );
+	} );
+
 	it( 'should be cloned properly', () => {
 		const root = new RootEditableElement( 'h1' );
 		root.document = createDocumentMock();

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

@@ -11,6 +11,7 @@ import Text from '../../src/view/text';
 import Position from '../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
+import createViewRoot from './_utils/createroot';
 import { parse } from '../../src/dev-utils/view';
 
 describe( 'Selection', () => {
@@ -878,7 +879,7 @@ describe( 'Selection', () => {
 		it( 'should return EditableElement when selection is placed inside', () => {
 			const viewDocument = new Document();
 			const selection = viewDocument.selection;
-			const root = viewDocument.createRoot( 'div' );
+			const root = createViewRoot( viewDocument, 'div', 'main' );
 			const element = new Element( 'p' );
 			root.appendChildren( element );
 

+ 2 - 3
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
 import Document from '../../src/view/document';
 import DocumentFragment from '../../src/view/documentfragment';
 import AttributeElement from '../../src/view/attributeelement';
@@ -13,6 +11,7 @@ import Text from '../../src/view/text';
 import TreeWalker from '../../src/view/treewalker';
 import Position from '../../src/view/position';
 import Range from '../../src/view/range';
+import createViewRoot from './_utils/createroot';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'TreeWalker', () => {
@@ -20,7 +19,7 @@ describe( 'TreeWalker', () => {
 
 	before( () => {
 		doc = new Document();
-		root = doc.createRoot( document.createElement( 'div' ) );
+		root = createViewRoot( doc );
 
 		// root
 		//  |- img1

+ 38 - 0
packages/ckeditor5-engine/tests/view/utils-tests/createroot.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Document from '../../../src/view/document.js';
+import RootAttributeElement from '../../../src/view/rooteditableelement.js';
+import createRoot from '../_utils/createroot.js';
+
+describe( 'createRoot', () => {
+	let viewDoc;
+
+	beforeEach( () => {
+		viewDoc = new Document();
+	} );
+
+	it( 'should create view root element with given data', () => {
+		const root = createRoot( viewDoc, 'h1', 'header' );
+
+		expect( root ).to.instanceof( RootAttributeElement );
+		expect( root.name ).to.equal( 'h1' );
+		expect( root.rootName ).to.equal( 'header' );
+	} );
+
+	it( 'should create view root element with default data', () => {
+		const root = createRoot( viewDoc );
+
+		expect( root ).to.instanceof( RootAttributeElement );
+		expect( root.name ).to.equal( 'div' );
+		expect( root.rootName ).to.equal( 'main' );
+	} );
+
+	it( 'should insert root element to view document roots collection', () => {
+		const root = createRoot( viewDoc );
+
+		expect( viewDoc.getRoot() ).to.equal( root );
+	} );
+} );