Sfoglia il codice sorgente

Merge branch 'master' into t/1436

Maciej Gołaszewski 7 anni fa
parent
commit
4b8f4969ce

+ 31 - 8
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -74,6 +74,14 @@ export function upcastElementToElement( config ) {
  * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
  * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
  *
+ * This helper is meant to set a model attribute on all the elements that are inside the converted element:
+ *
+ *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+ *
+ * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+ * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
+ * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute} for comparison.
+ *
  * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
  *
  *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
@@ -138,7 +146,7 @@ export function upcastElementToAttribute( config ) {
 
 	_normalizeModelAttributeConfig( config );
 
-	const converter = _prepareToAttributeConverter( config );
+	const converter = _prepareToAttributeConverter( config, false );
 
 	const elementName = _getViewElementNameFromConfig( config );
 	const eventName = elementName ? 'element:' + elementName : 'element';
@@ -154,6 +162,19 @@ export function upcastElementToAttribute( config ) {
  * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
  * `<image source="foo.jpg"></image>` in the model.
  *
+ * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
+ * is set only on the corresponding model node:
+ *
+ *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
+ *
+ * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
+ * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model:
+ *
+ *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+ *
+ * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+ * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
+ *
  * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
  *
  *		upcastAttributeToAttribute( { view: 'src', model: 'source' } );
@@ -223,7 +244,7 @@ export function upcastAttributeToAttribute( config ) {
 
 	_normalizeModelAttributeConfig( config, viewKey );
 
-	const converter = _prepareToAttributeConverter( config );
+	const converter = _prepareToAttributeConverter( config, true );
 
 	return dispatcher => {
 		dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
@@ -442,9 +463,9 @@ function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null )
 //
 // @param {String} modelAttributeKey The key of the model attribute to set on a model node.
 // @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
-// @param {Boolean} consumeName If set to `true` converter will try to consume name. If set to `false` converter will not try to
-// consume name. This flag overwrites parameter returned by `Matcher#match`.
-function _prepareToAttributeConverter( config ) {
+// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
+function _prepareToAttributeConverter( config, shallow ) {
 	const matcher = new Matcher( config.view );
 
 	return ( evt, data, conversionApi ) => {
@@ -483,7 +504,7 @@ function _prepareToAttributeConverter( config ) {
 		}
 
 		// Set attribute on current `output`. `Schema` is checked inside this helper function.
-		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, conversionApi );
+		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
 
 		if ( attributeWasSet ) {
 			conversionApi.consumable.consume( data.viewItem, match.match );
@@ -509,12 +530,14 @@ function _onlyViewNameIsDefined( config ) {
 // @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
 // @param {Object} modelAttribute Model attribute to set.
 // @param {Object} conversionApi Conversion API.
+// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
 // @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
-function _setAttributeOn( modelRange, modelAttribute, conversionApi ) {
+function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
 	let result = false;
 
 	// Set attribute on each item in range according to Schema.
-	for ( const node of Array.from( modelRange.getItems() ) ) {
+	for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
 		if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
 			conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
 

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

@@ -140,6 +140,14 @@ export default class Document {
 			}
 		} while ( wasFixed );
 	}
+
+	/**
+	 * Event fired whenever document content layout changes. It is fired whenever content is
+	 * {@link module:engine/view/view~View#event:render rendered}, but should be also fired by observers in case of
+	 * other actions which may change layout, for instance when image loads.
+	 *
+	 * @event layoutChanged
+	 */
 }
 
 mix( Document, ObservableMixin );

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

@@ -1,3 +1,4 @@
+
 /**
  * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.

+ 63 - 91
packages/ckeditor5-engine/src/view/renderer.js

@@ -218,7 +218,7 @@ export default class Renderer {
 
 			if ( !startsWithFiller( fillerDomPosition.parent ) ) {
 				// Filler has not been created at filler position. Create it now.
-				this._inlineFiller = this._addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset );
+				this._inlineFiller = addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset );
 			} else {
 				// Filler has been found, save it.
 				this._inlineFiller = fillerDomPosition.parent;
@@ -254,8 +254,12 @@ export default class Renderer {
 			return;
 		}
 
-		const diff = this._diffChildren( viewElement );
-		const actions = this._findReplaceActions( diff.actions, diff.actualDomChildren, diff.expectedDomChildren );
+		const actualDomChildren = this.domConverter.mapViewToDom( viewElement ).childNodes;
+		const expectedDomChildren = Array.from(
+			this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument )
+		);
+		const diff = this._diffNodeLists( actualDomChildren, expectedDomChildren );
+		const actions = this._findReplaceActions( diff, actualDomChildren, expectedDomChildren );
 
 		if ( actions.indexOf( 'replace' ) !== -1 ) {
 			const counter = { equal: 0, insert: 0, delete: 0 };
@@ -270,10 +274,10 @@ export default class Renderer {
 					// so we cannot use it with replacing flow (since it uses view children during rendering
 					// which will always result in rendering empty element).
 					if ( viewChild && !viewChild.is( 'uiElement' ) ) {
-						this._updateElementMappings( viewChild, diff.actualDomChildren[ deleteIndex ] );
+						this._updateElementMappings( viewChild, actualDomChildren[ deleteIndex ] );
 					}
 
-					remove( diff.expectedDomChildren[ insertIndex ] );
+					remove( expectedDomChildren[ insertIndex ] );
 					counter.equal++;
 				} else {
 					counter[ action ]++;
@@ -315,39 +319,6 @@ export default class Renderer {
 	}
 
 	/**
-	 * Adds inline filler at a given position.
-	 *
-	 * The position can be given as an array of DOM nodes and an offset in that array,
-	 * or a DOM parent element and an offset in that element.
-	 *
-	 * @private
-	 * @param {Document} domDocument
-	 * @param {Element|Array.<Node>} domParentOrArray
-	 * @param {Number} offset
-	 * @returns {Text} The DOM text node that contains an inline filler.
-	 */
-	_addInlineFiller( domDocument, domParentOrArray, offset ) {
-		const childNodes = domParentOrArray instanceof Array ? domParentOrArray : domParentOrArray.childNodes;
-		const nodeAfterFiller = childNodes[ offset ];
-
-		if ( isText( nodeAfterFiller ) ) {
-			nodeAfterFiller.data = INLINE_FILLER + nodeAfterFiller.data;
-
-			return nodeAfterFiller;
-		} else {
-			const fillerNode = domDocument.createTextNode( INLINE_FILLER );
-
-			if ( Array.isArray( domParentOrArray ) ) {
-				childNodes.splice( offset, 0, fillerNode );
-			} else {
-				insertAt( domParentOrArray, offset, fillerNode );
-			}
-
-			return fillerNode;
-		}
-	}
-
-	/**
 	 * Gets the position of the inline filler based on the current selection.
 	 * Here, we assume that we know that the filler is needed and
 	 * {@link #_isSelectionInInlineFiller is at the selection position}, and, since it is needed,
@@ -456,7 +427,7 @@ export default class Renderer {
 
 		// Prevent adding inline filler inside elements with contenteditable=false.
 		// https://github.com/ckeditor/ckeditor5-engine/issues/1170
-		if ( !_isEditable( selectionParent ) ) {
+		if ( !isEditable( selectionParent ) ) {
 			return false;
 		}
 
@@ -562,15 +533,24 @@ export default class Renderer {
 		}
 
 		const inlineFillerPosition = options.inlineFillerPosition;
-		// As binding may change actual DOM children we need to do this before diffing.
-		const expectedDomChildren = this._getElementExpectedChildren( viewElement, domElement, { bind: true, inlineFillerPosition } );
-		const diff = this._diffChildren( viewElement, inlineFillerPosition );
-		const actualDomChildren = diff.actualDomChildren;
+		const actualDomChildren = this.domConverter.mapViewToDom( viewElement ).childNodes;
+		const expectedDomChildren = Array.from(
+			this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true, inlineFillerPosition } )
+		);
+
+		// Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
+		// during diffing so text nodes could be compared correctly and also during rendering to maintain
+		// proper order and indexes while updating the DOM.
+		if ( inlineFillerPosition && inlineFillerPosition.parent === viewElement ) {
+			addInlineFiller( domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset );
+		}
+
+		const diff = this._diffNodeLists( actualDomChildren, expectedDomChildren );
 
 		let i = 0;
 		const nodesToUnbind = new Set();
 
-		for ( const action of diff.actions ) {
+		for ( const action of diff ) {
 			if ( action === 'insert' ) {
 				insertAt( domElement, i, expectedDomChildren[ i ] );
 				i++;
@@ -595,54 +575,15 @@ export default class Renderer {
 	}
 
 	/**
-	 * Compares view element's actual and expected children and returns an action sequence which can be used to transform
-	 * actual children into expected ones.
+	 * Shorthand for diffing two arrays or node lists of DOM nodes.
 	 *
 	 * @private
-	 * @param {module:engine/view/node~Node} viewElement The view element whose children will be compared.
-	 * @param {module:engine/view/position~Position} [inlineFillerPosition=null] The position where the inline
-	 * filler should be rendered.
-	 * @returns {Object|null} result
-	 * @returns {Array.<String>} result.actions List of actions based on {@link module:utils/diff~diff} function.
-	 * @returns {Array.<Node>} result.actualDomChildren Current view element's DOM children.
-	 * @returns {Array.<Node>} result.expectedDomChildren Expected view element's DOM children.
+	 * @param {Array.<Node>|NodeList} actualDomChildren Actual DOM children
+	 * @param {Array.<Node>|NodeList} expectedDomChildren Expected DOM children.
+	 * @returns {Array.<String>} The list of actions based on the {@link module:utils/diff~diff} function.
 	 */
-	_diffChildren( viewElement, inlineFillerPosition = null ) {
-		const domElement = this.domConverter.mapViewToDom( viewElement );
-		const actualDomChildren = domElement.childNodes;
-		const expectedDomChildren = this._getElementExpectedChildren( viewElement, domElement,
-			{ withChildren: false, inlineFillerPosition } );
-
-		return {
-			actions: diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFiller ) ),
-			actualDomChildren,
-			expectedDomChildren
-		};
-	}
-
-	/**
-	 * Returns expected DOM children for a given view element.
-	 *
-	 * @private
-	 * @param {module:engine/view/node~Node} viewElement View element whose children will be returned.
-	 * @param {Node} domElement The DOM representation of a given view element.
-	 * @param {Object} options See the {@link module:engine/view/domconverter~DomConverter#viewToDom} options parameter.
-	 * @param {module:engine/view/position~Position} [options.inlineFillerPosition=null] The position where
-	 * the inline filler should be rendered.
-	 * @returns {Array.<Node>} The view element's expected children.
-	 */
-	_getElementExpectedChildren( viewElement, domElement, options ) {
-		const expectedDomChildren = Array.from( this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, options ) );
-		const filler = options.inlineFillerPosition;
-
-		// Inline filler element has to be created as it is present in a DOM, but not in a view. It is required
-		// during diffing so text nodes could be compared correctly and also during rendering to maintain
-		// proper order and indexes while updating the DOM.
-		if ( filler && filler.parent === viewElement ) {
-			this._addInlineFiller( domElement.ownerDocument, expectedDomChildren, filler.offset );
-		}
-
-		return expectedDomChildren;
+	_diffNodeLists( actualDomChildren, expectedDomChildren ) {
+		return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFiller ) );
 	}
 
 	/**
@@ -656,7 +597,7 @@ export default class Renderer {
 	 *
 	 * @private
 	 * @param {Array.<String>} actions Actions array which is a result of the {@link module:utils/diff~diff} function.
-	 * @param {Array.<Node>} actualDom Actual DOM children
+	 * @param {Array.<Node>|NodeList} actualDom Actual DOM children
 	 * @param {Array.<Node>} expectedDom Expected DOM children.
 	 * @returns {Array.<String>} Actions array modified with the `replace` actions.
 	 */
@@ -903,7 +844,7 @@ mix( Renderer, ObservableMixin );
 // @private
 // @param {module:engine/view/element~Element} element
 // @returns {Boolean}
-function _isEditable( element ) {
+function isEditable( element ) {
 	if ( element.getAttribute( 'contenteditable' ) == 'false' ) {
 		return false;
 	}
@@ -913,6 +854,37 @@ function _isEditable( element ) {
 	return !parent || parent.getAttribute( 'contenteditable' ) == 'true';
 }
 
+// Adds inline filler at a given position.
+//
+// The position can be given as an array of DOM nodes and an offset in that array,
+// or a DOM parent element and an offset in that element.
+//
+// @private
+// @param {Document} domDocument
+// @param {Element|Array.<Node>} domParentOrArray
+// @param {Number} offset
+// @returns {Text} The DOM text node that contains an inline filler.
+function addInlineFiller( domDocument, domParentOrArray, offset ) {
+	const childNodes = domParentOrArray instanceof Array ? domParentOrArray : domParentOrArray.childNodes;
+	const nodeAfterFiller = childNodes[ offset ];
+
+	if ( isText( nodeAfterFiller ) ) {
+		nodeAfterFiller.data = INLINE_FILLER + nodeAfterFiller.data;
+
+		return nodeAfterFiller;
+	} else {
+		const fillerNode = domDocument.createTextNode( INLINE_FILLER );
+
+		if ( Array.isArray( domParentOrArray ) ) {
+			childNodes.splice( offset, 0, fillerNode );
+		} else {
+			insertAt( domParentOrArray, offset, fillerNode );
+		}
+
+		return fillerNode;
+	}
+}
+
 // Whether two DOM nodes should be considered as similar.
 // Nodes are considered similar if they have the same tag name.
 //

+ 3 - 0
packages/ckeditor5-engine/src/view/view.js

@@ -149,6 +149,9 @@ export default class View {
 		// Use 'normal' priority so that rendering is performed as first when using that priority.
 		this.on( 'render', () => {
 			this._render();
+
+			// Informs that layout has changed after render.
+			this.document.fire( 'layoutChanged' );
 		} );
 	}
 

+ 22 - 0
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -604,6 +604,28 @@ describe( 'Conversion', () => {
 
 				test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
 			} );
+
+			// #1443.
+			it( 'should not set attributes on the element\'s children', () => {
+				schema.register( 'div', {
+					inheritAllFrom: '$root',
+					allowWhere: '$block',
+					isLimit: true,
+					allowAttributes: [ 'border', 'shade' ]
+				} );
+
+				conversion.elementToElement(
+					{ model: 'div', view: 'div' }
+				);
+
+				conversion.attributeToAttribute( { model: 'border', view: { key: 'class', value: 'border' } } );
+				conversion.attributeToAttribute( { model: 'shade', view: { key: 'class', value: 'shade' } } );
+
+				test(
+					'<div class="border"><div class="shade"></div></div>',
+					'<div border="border"><div shade="shade"></div></div>'
+				);
+			} );
 		} );
 
 		function test( input, expectedModel, expectedView = null ) {

+ 48 - 0
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -349,6 +349,27 @@ describe( 'upcast-helpers', () => {
 				'<$text attribB="true" bold="true">foo</$text>'
 			);
 		} );
+
+		// #1443.
+		it( 'should set attributes on the element\'s children', () => {
+			const helperBold = upcastElementToAttribute( {
+				model: 'bold',
+				view: { name: 'strong' }
+			} );
+
+			const helperP = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+
+			conversion.for( 'upcast' ).add( helperP ).add( helperBold );
+
+			expectResult(
+				new ViewAttributeElement(
+					'strong',
+					null,
+					new ViewContainerElement( 'p', null, new ViewText( 'Foo' ) )
+				),
+				'<paragraph><$text bold="true">Foo</$text></paragraph>'
+			);
+		} );
 	} );
 
 	describe( 'upcastAttributeToAttribute', () => {
@@ -553,6 +574,33 @@ describe( 'upcast-helpers', () => {
 				'<image styled="true"></image>'
 			);
 		} );
+
+		// #1443.
+		it( 'should not set attributes on the element\'s children', () => {
+			schema.register( 'div', {
+				inheritAllFrom: '$root',
+				allowWhere: '$block',
+				isLimit: true,
+				allowAttributes: [ 'border', 'shade' ]
+			} );
+
+			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'div', model: 'div' } ) );
+
+			const shadeHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } );
+			const borderHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } );
+
+			conversion.for( 'upcast' ).add( shadeHelper );
+			conversion.for( 'upcast' ).add( borderHelper );
+
+			expectResult(
+				new ViewContainerElement(
+					'div',
+					{ class: 'border' },
+					new ViewContainerElement( 'div', { class: 'shade' } )
+				),
+				'<div border="border"><div shade="shade"></div></div>'
+			);
+		} );
 	} );
 
 	describe( 'upcastElementToMarker', () => {

+ 33 - 18
packages/ckeditor5-engine/tests/view/view/view.js

@@ -1,25 +1,25 @@
 /* globals document */
 
 import View from '../../../src/view/view';
+import Observer from '../../../src/view/observer/observer';
 import MutationObserver from '../../../src/view/observer/mutationobserver';
-import count from '@ckeditor/ckeditor5-utils/src/count';
 import KeyObserver from '../../../src/view/observer/keyobserver';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
 import FocusObserver from '../../../src/view/observer/focusobserver';
 import CompositionObserver from '../../../src/view/observer/compositionobserver';
-import createViewRoot from '../_utils/createroot';
-import Observer from '../../../src/view/observer/observer';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import ViewRange from '../../../src/view/range';
-import RootEditableElement from '../../../src/view/rooteditableelement';
 import ViewElement from '../../../src/view/element';
 import ViewPosition from '../../../src/view/position';
 import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import count from '@ckeditor/ckeditor5-utils/src/count';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import createViewRoot from '../_utils/createroot';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'view', () => {
 	const DEFAULT_OBSERVERS_COUNT = 6;
@@ -45,6 +45,7 @@ describe( 'view', () => {
 				this.enable = sinon.spy();
 				this.disable = sinon.spy();
 				this.observe = sinon.spy();
+				this.destroy = sinon.spy();
 			}
 		};
 
@@ -379,6 +380,20 @@ describe( 'view', () => {
 
 			sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
 		} );
+
+		it( 'should fire view.document.layoutChanged event on render', () => {
+			const spy = sinon.spy();
+
+			view.document.on( 'layoutChanged', spy );
+
+			view.render();
+
+			sinon.assert.calledOnce( spy );
+
+			view.render();
+
+			sinon.assert.calledTwice( spy );
+		} );
 	} );
 
 	describe( 'view and DOM integration', () => {
@@ -391,7 +406,7 @@ describe( 'view', () => {
 			const view = new View();
 			const viewDocument = view.document;
 
-			createRoot( 'div', 'main', viewDocument );
+			createViewRoot( viewDocument, 'div', 'main' );
 			view.attachDomRoot( domDiv );
 			view.render();
 
@@ -407,7 +422,7 @@ describe( 'view', () => {
 
 			const view = new View();
 			const viewDocument = view.document;
-			createRoot( 'div', 'main', viewDocument );
+			createViewRoot( viewDocument, 'div', 'main' );
 			view.attachDomRoot( domDiv );
 
 			viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
@@ -424,7 +439,7 @@ describe( 'view', () => {
 
 			const view = new View();
 			const viewDocument = view.document;
-			const viewRoot = createRoot( 'div', 'main', viewDocument );
+			const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
 
 			view.attachDomRoot( domRoot );
 
@@ -588,13 +603,13 @@ describe( 'view', () => {
 		} );
 	} );
 
-	function createRoot( name, rootName, viewDoc ) {
-		const viewRoot = new RootEditableElement( name );
+	describe( 'destroy()', () => {
+		it( 'should destroy all observers', () => {
+			const observerMock = view.addObserver( ObserverMock );
 
-		viewRoot.rootName = rootName;
-		viewRoot._document = viewDoc;
-		viewDoc.roots.add( viewRoot );
+			view.destroy();
 
-		return viewRoot;
-	}
+			sinon.assert.calledOnce( observerMock.destroy );
+		} );
+	} );
 } );