浏览代码

Merge pull request #539 from ckeditor/t/439

DataProcessor working between data and view
Piotr Jasiun 9 年之前
父节点
当前提交
e6f050a5c0

+ 6 - 22
packages/ckeditor5-engine/src/datacontroller.js

@@ -12,8 +12,6 @@ import ViewConversionDispatcher from './conversion/viewconversiondispatcher.js';
 import { convertText, convertToModelFragment } from './conversion/view-to-model-converters.js';
 
 import ViewDocumentFragment from './view/documentfragment.js';
-import DomConverter from './view/domconverter.js';
-import { NBSP_FILLER } from './view/filler.js';
 
 import ModelRange from './model/range.js';
 import ModelPosition from './model/position.js';
@@ -65,14 +63,6 @@ export default class DataController {
 		this._mapper = new Mapper();
 
 		/**
-		 * DOM converter used during the conversion.
-		 *
-		 * @private
-		 * @member {engine.view.DomConverter} engine.DataController#_domConverter
-		 */
-		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
-
-		/**
 		 * Model to view conversion dispatcher used by the {@link engine.DataController#get get method}.
 		 * To attach model to view converter to the data pipeline you need to add lister to this property:
 		 *
@@ -129,7 +119,7 @@ export default class DataController {
 		const modelRoot = this.model.getRoot( rootName );
 		const modelRange = ModelRange.createFromElement( modelRoot );
 
-		// model -> view.
+		// model -> view
 		const viewDocumentFragment = new ViewDocumentFragment();
 		this._mapper.bindElements( modelRoot, viewDocumentFragment );
 
@@ -137,11 +127,8 @@ export default class DataController {
 
 		this._mapper.clearBindings();
 
-		// view -> DOM.
-		const domDocumentFragment = this._domConverter.viewToDom( viewDocumentFragment, document );
-
-		// DOM -> data.
-		return this.processor.toData( domDocumentFragment );
+		// view -> data
+		return this.processor.toData( viewDocumentFragment );
 	}
 
 	/**
@@ -175,13 +162,10 @@ export default class DataController {
 	 * @returns {engine.model.DocumentFragment} Parsed data.
 	 */
 	parse( data ) {
-		// data -> DOM.
-		const domDocumentFragment = this.processor.toDom( data );
-
-		// DOM -> view.
-		const viewDocumentFragment = this._domConverter.domToView( domDocumentFragment );
+		// data -> view
+		const viewDocumentFragment = this.processor.toView( data );
 
-		// view -> model.
+		// view -> model
 		const modelDocumentFragment = this.viewToModel.convert( viewDocumentFragment, { context: [ '$root' ] } );
 
 		return modelDocumentFragment;

+ 6 - 6
packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc

@@ -10,23 +10,23 @@
 /**
  * DataProcessor interface. It should be implemented by actual DataProcessors.
  * Each data processor implements a certain format of the data. E.g. `MarkdownDataProcessor` will convert the data
- * (Markdown string) to a DocumentFragment and back.
+ * (Markdown string) to a {@link engine.view.DocumentFragment DocumentFragment} and back.
  *
  * @interface engine.dataProcessor.DataProcessor
  */
 
 /**
- * Converts a DocumentFragment to data.
+ * Converts a {@link engine.view.DocumentFragment DocumentFragment} to data.
  *
  * @method engine.dataProcessor.DataProcessor#toData
- * @param {DocumentFragment} fragment DocumentFragment to be processed.
+ * @param {engine.view.DocumentFragment} fragment DocumentFragment to be processed.
  * @returns {*}
  */
 
 /**
- * Converts data to a DocumentFragment.
+ * Converts data to a {@link engine.view.DocumentFragment DocumentFragment}.
  *
- * @method engine.dataProcessor.DataProcessor#toDom
+ * @method engine.dataProcessor.DataProcessor#toView
  * @param {*} data Data to be processed.
- * @returns {DocumentFragment}
+ * @returns {engine.view.DocumentFragment DocumentFragment}
  */

+ 35 - 6
packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js

@@ -4,6 +4,8 @@
  */
 
 import BasicHtmlWriter from './basichtmlwriter.js';
+import DomConverter from '../view/domconverter.js';
+import { NBSP_FILLER } from '../view/filler.js';
 
 /**
  * HtmlDataProcessor class.
@@ -26,6 +28,14 @@ export default class HtmlDataProcessor {
 		this._domParser = new DOMParser();
 
 		/**
+		 * DOM converter used to convert DOM elements to view elements.
+		 *
+		 * @private
+		 * @member {engine.view.DomConverter} engine.dataProcessor.HtmlDataProcessor#_domConverter.
+		 */
+		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+
+		/**
 		 * BasicHtmlWriter instance used to convert DOM elements to HTML string.
 		 *
 		 * @private
@@ -35,23 +45,42 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided document fragment to data format - in this case HTML string.
+	 * Converts provided {@link engine.view.DocumentFragment DocumentFragment} to data format - in this case HTML string.
+	 *
+	 * @param {engine.view.DocumentFragment} viewFragment
+	 * @returns {String} HTML string.
+	 */
+	toData( viewFragment ) {
+		// Convert view DocumentFragment to DOM DocumentFragment.
+		const domFragment = this._domConverter.viewToDom( viewFragment, document );
+
+		// Convert DOM DocumentFragment to HTML output.
+		return this._htmlWriter.getHtml( domFragment );
+	}
+
+	/**
+	 * Converts provided HTML string to view tree.
 	 *
-	 * @param {DocumentFragment} fragment
-	 * @returns {String}
+	 * @param {String} data HTML string.
+	 * @returns {engine.view.Node|engine.view.DocumentFragment|null} Converted view element.
 	 */
-	toData( fragment ) {
-		return this._htmlWriter.getHtml( fragment );
+	toView( data ) {
+		// Convert input HTML data to DOM DocumentFragment.
+		const domFragment = this._toDom( data );
+
+		// Convert DOM DocumentFragment to view DocumentFragment.
+		return this._domConverter.domToView( domFragment );
 	}
 
 	/**
 	 * Converts HTML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
 	 * provided data.
 	 *
+	 * @private
 	 * @param {String} data
 	 * @returns {DocumentFragment}
 	 */
-	toDom( data ) {
+	_toDom( data ) {
 		const document = this._domParser.parseFromString( data, 'text/html' );
 		const fragment = document.createDocumentFragment();
 		const nodes = document.body.childNodes;

+ 168 - 215
packages/ckeditor5-engine/tests/_utils/view.js

@@ -260,16 +260,28 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  */
 export function parse( data, options = {} ) {
 	options.order = options.order || [];
-	const viewParser = new ViewParser();
 	const rangeParser = new RangeParser();
+	const processor = new HtmlDataProcessor();
 
-	let view = viewParser.parse( data, options.rootElement );
+	// Convert data to view.
+	let view = processor.toView( data );
 
-	// If single Element or Text is returned - move it to the DocumentFragment.
-	if ( !options.rootElement && ( view instanceof ViewText || view instanceof ViewElement ) ) {
-		view = new ViewDocumentFragment( view );
+	// At this point we have a view tree with Elements that could have names like `attribute:b:1`. In the next step
+	// we need to parse Element's names and convert them to AttributeElements and ContainerElements.
+	view = _convertViewElements( view );
+
+	// If custom root is provided - move all nodes there.
+	if ( options.rootElement ) {
+		const root = options.rootElement;
+		const nodes = view.removeChildren( 0, view.getChildCount() );
+
+		root.removeChildren( 0, root.getChildCount() );
+		root.appendChildren( nodes );
+
+		view = root;
 	}
 
+	// Parse ranges included in view text nodes.
 	const ranges = rangeParser.parse( view, options.order );
 
 	// If only one element is returned inside DocumentFragment - return that element.
@@ -492,216 +504,6 @@ class RangeParser {
 }
 
 /**
- * Private helper class used to convert given HTML-like string to view tree.
- *
- * @private
- */
-class ViewParser {
-	/**
-	 * Parses HTML-like string to view tree elements.
-	 *
-	 * @param {String} data
-	 * @param {engine.view.Element|engine.view.DocumentFragment} [rootElement=null] Default root to use when parsing elements.
-	 * When set to `null` root element will be created automatically. If set to
-	 * {@link engine.view.Element Element} or {@link engine.view.DocumentFragment DocumentFragment} - this node
-	 * will be used as root for all parsed nodes.
-	 * @returns {engine.view.Node|engine.view.DocumentFragment}
-	 */
-	parse( data, rootElement = null ) {
-		const htmlProcessor = new HtmlDataProcessor();
-
-		// Convert HTML string to DOM.
-		const domRoot = htmlProcessor.toDom( data );
-
-		// If root element is provided - remove all children from it.
-		if ( rootElement ) {
-			rootElement.removeChildren( 0, rootElement.getChildCount() );
-		}
-
-		// Convert DOM to View.
-		return this._walkDom( domRoot, rootElement );
-	}
-
-	/**
-	 * Walks through DOM elements and converts them to tree view elements.
-	 *
-	 * @private
-	 * @param {Node} domNode
-	 * @param {engine.view.Element|engine.view.DocumentFragment} [rootElement=null] Default root element to use
-	 * when parsing elements.
-	 * @returns {engine.view.Node|engine.view.DocumentFragment}
-	 */
-	_walkDom( domNode, rootElement = null ) {
-		const isDomElement = domNode instanceof window.Element;
-
-		if ( isDomElement || domNode instanceof window.DocumentFragment ) {
-			const children = domNode.childNodes;
-			const length = children.length;
-
-			// If there is only one element inside DOM DocumentFragment - use it as root.
-			if ( !isDomElement && length == 1 ) {
-				return this._walkDom( domNode.childNodes[ 0 ], rootElement );
-			}
-
-			let viewElement;
-
-			if ( isDomElement ) {
-				viewElement = this._convertElement( domNode );
-
-				if ( rootElement ) {
-					rootElement.appendChildren( viewElement );
-				}
-			} else {
-				viewElement = rootElement ? rootElement : new ViewDocumentFragment();
-			}
-
-			for ( let i = 0; i < children.length; i++ ) {
-				const child = children[ i ];
-				viewElement.appendChildren( this._walkDom( child ) );
-			}
-
-			return rootElement ? rootElement : viewElement;
-		}
-
-		return new ViewText( domNode.textContent );
-	}
-
-	/**
-	 * Converts DOM Element to {engine.view.Element view Element}.
-	 *
-	 * @private
-	 * @param {Element} domElement DOM element to convert.
-	 * @returns {engine.view.Element|engine.view.AttributeElement|engine.view.ContainerElement} Tree view
-	 * element converted from DOM element.
-	 */
-	_convertElement( domElement ) {
-		const info = this._convertElementName( domElement );
-		let viewElement;
-
-		if ( info.type == 'attribute' ) {
-			viewElement = new AttributeElement( info.name );
-
-			if ( info.priority !== null ) {
-				viewElement.priority = info.priority;
-			}
-		} else if ( info.type == 'container' ) {
-			viewElement = new ContainerElement( info.name );
-		} else {
-			viewElement = new ViewElement( info.name );
-		}
-
-		const attributes = domElement.attributes;
-		const attributesCount = attributes.length;
-
-		for ( let i = 0; i < attributesCount; i++ ) {
-			const attribute = attributes[ i ];
-			viewElement.setAttribute( attribute.name, attribute.value );
-		}
-
-		return viewElement;
-	}
-
-	/**
-	 * Converts DOM element tag name to information needed for creating {@link engine.view.Element view Element} instance.
-	 * Name can be provided in couple formats: as a simple element's name (`div`), as a type and name (`container:div`,
-	 * `attribute:span`), as a name and priority (`span:12`) and as a type, priority, name trio (`attribute:span:12`);
-	 *
-	 * @private
-	 * @param {Element} element DOM Element which tag name should be converted.
-	 * @returns {Object} info Object with parsed information.
-	 * @returns {String} info.name Parsed name of the element.
-	 * @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
-	 * @returns {Number|null} info.priority Parsed priority of the element.
-	 */
-	_convertElementName( element ) {
-		const parts = element.tagName.toLowerCase().split( ':' );
-
-		if ( parts.length == 1 ) {
-			return {
-				name: parts[ 0 ],
-				type: null,
-				priority: null
-			};
-		}
-
-		if ( parts.length == 2 ) {
-			// Check if type and name: container:div.
-			const type = this._convertType( parts[ 0 ] );
-
-			if ( type ) {
-				return {
-					name: parts[ 1 ],
-					type: type,
-					priority: null
-				};
-			}
-
-			// Check if name and priority: span:10.
-			const priority = this._convertPriority( parts[ 1 ] );
-
-			if ( priority !== null ) {
-				return {
-					name: parts[ 0 ],
-					type: 'attribute',
-					priority: priority
-				};
-			}
-
-			throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
-		}
-
-		// Check if name is in format type:name:priority.
-		if ( parts.length === 3 ) {
-			const type = this._convertType( parts[ 0 ] );
-			const priority = this._convertPriority( parts[ 2 ] );
-
-			if ( type && priority !== null ) {
-				return {
-					name: parts[ 1 ],
-					type: type,
-					priority: priority
-				};
-			}
-		}
-
-		throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
-	}
-
-	/**
-	 * Checks if element's type is allowed. Returns `attribute`, `container` or `null`.
-	 *
-	 * @private
-	 * @param {String} type
-	 * @returns {String|null}
-	 */
-	_convertType( type ) {
-		if ( type == 'container' || type == 'attribute' ) {
-			return type;
-		}
-
-		return null;
-	}
-
-	/**
-	 * Checks if given priority is allowed. Returns null if priority cannot be converted.
-	 *
-	 * @private
-	 * @param {String} priorityString
-	 * @returns {Number|Null}
-	 */
-	_convertPriority( priorityString ) {
-		const priority = parseInt( priorityString, 10 );
-
-		if ( !isNaN( priority ) ) {
-			return priority;
-		}
-
-		return null;
-	}
-
-}
-
-/**
  * Private helper class used for converting view tree to string.
  *
  * @private
@@ -956,3 +758,154 @@ class ViewStringify {
 		return attributes.join( ' ' );
 	}
 }
+
+// Converts {@link engine.view.Element Elements} to {@link engine.view.AttributeElement AttributeElements} and
+// {@link engine.view.ContainerElement ContainerElements}. It converts whole tree starting from the `rootNode`.
+// Conversion is based on element names. See `_convertElement` method for more details.
+//
+// @param {engine.view.Element|engine.view.DocumentFragment|engine.view.Text} rootNode Root node to convert.
+// @returns {engine.view.Element|engine.view.DocumentFragment|engine.view.Text|engine.view.AttributeElement|
+// engine.view.ContainerElement} Root node of converted elements.
+function _convertViewElements( rootNode ) {
+	const isFragment = rootNode instanceof ViewDocumentFragment;
+
+	if ( rootNode instanceof ViewElement || isFragment ) {
+		// Convert element or leave document fragment.
+		const convertedElement = isFragment ? new ViewDocumentFragment() : _convertElement( rootNode );
+
+		// Convert all child nodes.
+		for ( let child of rootNode.getChildren() ) {
+			convertedElement.appendChildren( _convertViewElements( child ) );
+		}
+
+		return convertedElement;
+	}
+
+	return rootNode;
+}
+
+// Converts {@link engine.view.Element Element} to {@link engine.view.AttributeElement AttributeElement} or
+// {@link engine.view.ContainerElement ContainerElement}.
+// If element's name is in format `attribute:b:1` or `b:11` it will be converted to
+// {@link engine.view.AttributeElement AttributeElement} with priority 11.
+// If element's name is in format `container:p` - it will be converted to
+// {@link engine.view.ContainerElement ContainerElement}.
+// If element's name will not contain any additional information - {@link engine.view.Element view Element}will be
+// returned.
+//
+// @param {engine.view.Element} viewElement View element to convert.
+// @returns {engine.view.Element|engine.view.AttributeElement|engine.view.ContainerElement} Tree view
+// element converted according to it's name.
+function _convertElement( viewElement ) {
+	const info = _convertElementName( viewElement );
+	let newElement;
+
+	if ( info.type == 'attribute' ) {
+		newElement = new AttributeElement( info.name );
+
+		if ( info.priority !== null ) {
+			newElement.priority = info.priority;
+		}
+	} else if ( info.type == 'container' ) {
+		newElement = new ContainerElement( info.name );
+	} else {
+		newElement = new ViewElement( info.name );
+	}
+
+	// Move attributes.
+	for ( let attributeKey of viewElement.getAttributeKeys() ) {
+		newElement.setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
+	}
+
+	return newElement;
+}
+
+// Converts {@link engine.view.Element#name Element's name} information needed for creating
+// {@link engine.view.AttributeElement AttributeElement} or {@link engine.view.ContainerElement ContainerElement} instance.
+// Name can be provided in couple formats: as a simple element's name (`div`), as a type and name (`container:div`,
+// `attribute:span`), as a name and priority (`span:12`) and as a type, priority, name trio (`attribute:span:12`);
+//
+// @param {engine.view.Element} element Element which name should be converted.
+// @returns {Object} info Object with parsed information.
+// @returns {String} info.name Parsed name of the element.
+// @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
+// returns {Number|null} info.priority Parsed priority of the element.
+function _convertElementName( viewElement ) {
+	const parts = viewElement.name.split( ':' );
+
+	if ( parts.length == 1 ) {
+		return {
+			name: parts[ 0 ],
+			type: null,
+			priority: null
+		};
+	}
+
+	if ( parts.length == 2 ) {
+		// Check if type and name: container:div.
+		const type = _convertType( parts[ 0 ] );
+
+		if ( type ) {
+			return {
+				name: parts[ 1 ],
+				type: type,
+				priority: null
+			};
+		}
+
+		// Check if name and priority: span:10.
+		const priority = _convertPriority( parts[ 1 ] );
+
+		if ( priority !== null ) {
+			return {
+				name: parts[ 0 ],
+				type: 'attribute',
+				priority: priority
+			};
+		}
+
+		throw new Error( `Parse error - cannot parse element's name: ${ viewElement.name }.` );
+	}
+
+	// Check if name is in format type:name:priority.
+	if ( parts.length === 3 ) {
+		const type = _convertType( parts[ 0 ] );
+		const priority = _convertPriority( parts[ 2 ] );
+
+		if ( type && priority !== null ) {
+			return {
+				name: parts[ 1 ],
+				type: type,
+				priority: priority
+			};
+		}
+	}
+
+	throw new Error( `Parse error - cannot parse element's tag name: ${ viewElement.name }.` );
+}
+
+// Checks if element's type is allowed. Returns `attribute`, `container` or `null`.
+//
+// @param {String} type
+// @returns {String|null}
+function _convertType( type ) {
+	if ( type == 'container' || type == 'attribute' ) {
+		return type;
+	}
+
+	return null;
+}
+
+// Checks if given priority is allowed. Returns null if priority cannot be converted.
+//
+// @param {String} priorityString
+// returns {Number|Null}
+function _convertPriority( priorityString ) {
+	const priority = parseInt( priorityString, 10 );
+
+	if ( !isNaN( priority ) ) {
+		return priority;
+	}
+
+	return null;
+}

+ 34 - 35
packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js

@@ -7,49 +7,41 @@
 
 import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
 import xssTemplates from '/tests/engine/dataprocessor/_utils/xsstemplates.js';
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'HtmlDataProcessor', () => {
 	const dataProcessor = new HtmlDataProcessor();
 
-	describe( 'toDom', () => {
+	describe( 'toView', () => {
 		it( 'should return empty DocumentFragment when empty string is passed', () => {
-			const fragment = dataProcessor.toDom( '' );
-			expect( fragment ).to.be.an.instanceOf( DocumentFragment );
-			expect( fragment.childNodes.length ).to.equal( 0 );
+			const fragment = dataProcessor.toView( '' );
+			expect( fragment ).to.be.an.instanceOf( ViewDocumentFragment );
+			expect( fragment.getChildCount() ).to.equal( 0 );
 		} );
 
 		it( 'should convert HTML to DocumentFragment with single text node', () => {
-			const fragment = dataProcessor.toDom( 'foo bar' );
-			expect( fragment.childNodes.length ).to.equal( 1 );
-			expect( fragment.childNodes[ 0 ].nodeType ).to.equal( Node.TEXT_NODE );
-			expect( fragment.childNodes[ 0 ].textContent ).to.equal( 'foo bar' );
+			const fragment = dataProcessor.toView( 'foo bar' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo bar' );
 		} );
 
 		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
-			const fragment = dataProcessor.toDom( '<p>foo</p><p>bar</p>' );
-			expect( fragment.childNodes.length ).to.equal( 2 );
-			expect( fragment.childNodes[ 0 ].nodeType ).to.equal( Node.ELEMENT_NODE );
-			expect( fragment.childNodes[ 0 ].textContent ).to.equal( 'foo' );
-			expect( fragment.childNodes[ 1 ].nodeType ).to.equal( Node.ELEMENT_NODE );
-			expect( fragment.childNodes[ 1 ].textContent ).to.equal( 'bar' );
+			const fragment = dataProcessor.toView( '<p>foo</p><p>bar</p>' );
+
+			expect( stringify( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
 
 		it( 'should return only elements inside body tag', () => {
-			const fragment = dataProcessor.toDom( '<html><head></head><body><p>foo</p></body></html>' );
-			expect( fragment.childNodes.length ).to.equal( 1 );
-			expect( fragment.childNodes[ 0 ].textContent ).to.equal( 'foo' );
-			expect( fragment.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'p' );
+			const fragment = dataProcessor.toView( '<html><head></head><body><p>foo</p></body></html>' );
+
+			expect( stringify( fragment ) ).to.equal( '<p>foo</p>' );
 		} );
 
 		it( 'should not add any additional nodes', () => {
-			const fragment = dataProcessor.toDom( 'foo <b>bar</b> text' );
-			expect( fragment.childNodes.length ).to.equal( 3 );
-			expect( fragment.childNodes[ 0 ].nodeType ).to.equal( Node.TEXT_NODE );
-			expect( fragment.childNodes[ 0 ].textContent ).to.equal( 'foo ' );
-			expect( fragment.childNodes[ 1 ].nodeType ).to.equal( Node.ELEMENT_NODE );
-			expect( fragment.childNodes[ 1 ].innerHTML ).to.equal( 'bar' );
-			expect( fragment.childNodes[ 2 ].nodeType ).to.equal( Node.TEXT_NODE );
-			expect( fragment.childNodes[ 2 ].textContent ).to.equal( ' text' );
+			const fragment = dataProcessor.toView( 'foo <b>bar</b> text' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo <b>bar</b> text' );
 		} );
 
 		// Test against XSS attacks.
@@ -58,7 +50,7 @@ describe( 'HtmlDataProcessor', () => {
 
 			it( 'should prevent XSS attacks: ' + name, ( done ) => {
 				window.testXss = sinon.spy();
-				dataProcessor.toDom( input );
+				dataProcessor.toView( input );
 
 				setTimeout( () => {
 					sinon.assert.notCalled( window.testXss );
@@ -69,16 +61,23 @@ describe( 'HtmlDataProcessor', () => {
 	} );
 
 	describe( 'toData', () => {
-		it( 'should use HtmlWriter', () => {
-			const spy = sinon.spy( dataProcessor._htmlWriter, 'getHtml' );
+		it( 'should return empty string when empty DocumentFragment is passed', () => {
+			const fragment = new ViewDocumentFragment();
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( '' );
+		} );
+
+		it( 'should return text if document fragment with single text node is passed', () => {
+			const fragment = new ViewDocumentFragment();
+			fragment.appendChildren( parse( 'foo bar' ) );
 
-			const fragment = document.createDocumentFragment();
-			const paragraph = document.createElement( 'p' );
-			fragment.appendChild( paragraph );
-			dataProcessor.toData( fragment );
+			expect( dataProcessor.toData( fragment ) ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
+			const fragment = parse( '<p>foo</p><p>bar</p>' );
 
-			spy.restore();
-			sinon.assert.calledWithExactly( spy, fragment );
+			expect( dataProcessor.toData( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
 	} );
 } );