Sfoglia il codice sorgente

Changed: unified values returned in data.output during view-to-model conversion.

Szymon Cofalik 8 anni fa
parent
commit
16a08399c5

+ 60 - 23
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -8,9 +8,6 @@
  */
 
 import ViewConsumable from './viewconsumable';
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
 import ModelTreeWalker from '../model/treewalker';
@@ -18,6 +15,11 @@ import ModelNode from '../model/node';
 import ModelDocumentFragment from '../model/documentfragment';
 import { remove } from '../model/writer';
 
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
 /**
  * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
  * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
@@ -130,36 +132,34 @@ export default class ViewConversionDispatcher {
 	 * @fires element
 	 * @fires text
 	 * @fires documentFragment
-	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
-	 * viewItem Part of the view to be converted.
+	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
+	 * Part of the view to be converted.
 	 * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
 	 * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
-	 * wrapped by DocumentFragment. Converted marker stamps will be set as DocumentFragment
+	 * wrapped in `DocumentFragment`. Converted marker stamps will be set as that document fragment's
 	 * {@link module:engine/view/documentfragment~DocumentFragment#markers static markers map}.
 	 */
 	convert( viewItem, additionalData = {} ) {
 		this.fire( 'viewCleanup', viewItem );
 
 		const consumable = ViewConsumable.createFrom( viewItem );
-		const conversionResult = this._convertItem( viewItem, consumable, additionalData );
+		let conversionResult = this._convertItem( viewItem, consumable, additionalData );
 
-		// In some cases conversion output doesn't have to be a node and in this case we do nothing additional with this data.
-		if ( !( conversionResult instanceof ModelNode || conversionResult instanceof ModelDocumentFragment ) ) {
-			return conversionResult;
+		// If conversion failed, `null` will be returned.
+		if ( !conversionResult ) {
+			return new ModelDocumentFragment();
 		}
 
-		let documentFragment = conversionResult;
-
-		// When conversion result is not a DocumentFragment we need to wrap it by DocumentFragment.
-		if ( !documentFragment.is( 'documentFragment' ) ) {
-			documentFragment = new ModelDocumentFragment( [ documentFragment ] );
+		// When conversion result is not a document fragment we need to wrap it in document fragment.
+		if ( !conversionResult.is( 'documentFragment' ) ) {
+			conversionResult = new ModelDocumentFragment( [ conversionResult ] );
 		}
 
 		// Extract temporary markers stamp from model and set as static markers collection.
-		documentFragment.markers = extractMarkersFromModelFragment( documentFragment );
+		conversionResult.markers = extractMarkersFromModelFragment( conversionResult );
 
-		return documentFragment;
+		return conversionResult;
 	}
 
 	/**
@@ -168,8 +168,7 @@ export default class ViewConversionDispatcher {
 	 */
 	_convertItem( input, consumable, additionalData = {} ) {
 		const data = extend( {}, additionalData, {
-			input: input,
-			output: null
+			input: input
 		} );
 
 		if ( input.is( 'element' ) ) {
@@ -180,6 +179,30 @@ export default class ViewConversionDispatcher {
 			this.fire( 'documentFragment', data, consumable, this.conversionApi );
 		}
 
+		// Handle incorrect `data.output`.
+		if ( data.output === undefined ) {
+			/**
+			 * Item has not been converted (output is undefined).
+			 *
+			 * @error view-conversion-dispatcher-item-not-converted
+			 */
+			log.warn( 'view-conversion-dispatcher-item-not-converted: Item has not been converted (output is undefined).', input );
+
+			data.output = null;
+		} else if ( !( data.output instanceof ModelNode || data.output instanceof ModelDocumentFragment ) ) {
+			/**
+			 * Dropped incorrect conversion result.
+			 *
+			 * Item may be converted to either {@link module:engine/model/node~Node model node} or
+			 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
+			 *
+			 * @error view-conversion-dispatcher-incorrect-result
+			 */
+			log.warn( 'view-conversion-dispatcher-incorrect-result: Dropped incorrect conversion result.', [ input, data.output ] );
+
+			data.output = null;
+		}
+
 		return data.output;
 	}
 
@@ -188,11 +211,23 @@ export default class ViewConversionDispatcher {
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
 	 */
 	_convertChildren( input, consumable, additionalData = {} ) {
+		// Get all children of view input item.
 		const viewChildren = Array.from( input.getChildren() );
-		const convertedChildren = viewChildren.map( ( viewChild ) => this._convertItem( viewChild, consumable, additionalData ) );
 
-		// Flatten and remove nulls.
-		return convertedChildren.reduce( ( a, b ) => b ? a.concat( b ) : a, [] );
+		// 1. Map those children to model.
+		// 2. Filter out wrong results.
+		// 3. Extract children from document fragments to flatten results.
+		const convertedChildren = viewChildren
+			.map( ( viewChild ) => this._convertItem( viewChild, consumable, additionalData ) )
+			.filter( ( converted ) => converted instanceof ModelNode || converted instanceof ModelDocumentFragment )
+			.reduce( ( result, filtered ) => {
+				return result.concat(
+					filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered
+				);
+			}, [] );
+
+		// Normalize array to model document fragment.
+		return new ModelDocumentFragment( convertedChildren );
 	}
 
 	/**
@@ -304,6 +339,8 @@ function extractMarkersFromModelFragment( modelItem ) {
  *
  * Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or
  * modify that property. When all callbacks are done, the final value of `output` property is returned by this method.
+ * The `output` must be either {@link module:engine/model/node~Node model node} or
+ * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
  *
  * @method #convertItem
  * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
@@ -329,5 +366,5 @@ function extractMarkersFromModelFragment( modelItem ) {
  * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
- * @returns {Array.<*>} Array containing results of conversion of all children of given item.
+ * @returns {module:engine/model/documentfragment} Model document fragment containing results of conversion of all children of given item.
  */

+ 7 - 4
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -519,7 +519,7 @@ describe( 'advanced-converters', () => {
 		it( 'should convert a view element to model', () => {
 			let viewElement = new ViewAttributeElement( 'a', { href: 'foo.html', title: 'Foo title' }, new ViewText( 'foo' ) );
 
-			let modelText = viewDispatcher.convert( viewElement )[ 0 ];
+			let modelText = viewDispatcher.convert( viewElement ).getChild( 0 );
 
 			expect( modelText ).to.be.instanceof( ModelText );
 			expect( modelText.data ).to.equal( 'foo' );
@@ -603,11 +603,14 @@ describe( 'advanced-converters', () => {
 		viewDispatcher.on( 'element:tr', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
 				data.output = new ModelElement( 'paragraph' );
+
 				const children = conversionApi.convertChildren( data.input, consumable );
 
-				for ( let i = 1; i < children.length; i++ ) {
-					if ( children[ i ] instanceof ModelText && children[ i - 1 ] instanceof ModelText ) {
-						children.splice( i, 0, new ModelText( ' ' ) );
+				for ( let i = 1; i < children.childCount; i++ ) {
+					const child = children.getChild( i );
+
+					if ( child instanceof ModelText && child.previousSibling instanceof ModelText ) {
+						children.insertChildren( i, new ModelText( ' ' ) );
 						i++;
 					}
 				}

+ 18 - 2
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -6,6 +6,7 @@
 import buildViewConverter from '../../src/conversion/buildviewconverter';
 
 import ModelSchema from '../../src/model/schema';
+import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelDocument from '../../src/model/document';
 import ModelElement from '../../src/model/element';
 import ModelTextProxy from '../../src/model/textproxy';
@@ -281,7 +282,10 @@ describe( 'View converter builder', () => {
 
 		const element = new ViewAttributeElement( 'span' );
 
-		expect( dispatcher.convert( element, objWithContext ) ).to.null;
+		const result = dispatcher.convert( element, objWithContext );
+
+		expect( result ).to.be.instanceof( ModelDocumentFragment );
+		expect( result.childCount ).to.equal( 0 );
 	} );
 
 	it( 'should throw an error when view element in not valid to convert to marker', () => {
@@ -392,6 +396,16 @@ describe( 'View converter builder', () => {
 		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
 	} );
 
+	it( 'should return model document fragment when converting attributes on text', () => {
+		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
+
+		let viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
+
+		let conversionResult = dispatcher.convert( viewElement, objWithContext );
+
+		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
+	} );
+
 	it( 'should set different priorities for `toElement` and `toAttribute` conversion', () => {
 		buildViewConverter().for( dispatcher )
 			.fromAttribute( 'class' )
@@ -527,7 +541,9 @@ describe( 'View converter builder', () => {
 
 		viewElement.setAttribute( 'stop', true );
 		conversionResult = dispatcher.convert( viewElement, objWithContext );
-		expect( conversionResult ).to.be.null;
+
+		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+		expect( conversionResult.childCount ).to.equal( 0 );
 	} );
 
 	it( 'should stop to attribute conversion if creating function returned null', () => {

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

@@ -66,11 +66,13 @@ describe( 'view-to-model-converters', () => {
 
 			let conversionResult = dispatcher.convert( viewText, objWithContext );
 
-			expect( conversionResult ).to.be.null;
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 0 );
 
 			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 1 );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
 			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
 		} );

+ 188 - 126
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -5,7 +5,6 @@
 
 import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
 import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
@@ -14,7 +13,16 @@ import ModelElement from '../../src/model/element';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import { stringify } from '../../src/dev-utils/model';
 
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+// Stored in case it is silenced and has to be restored.
+const logWarn = log.warn;
+
 describe( 'ViewConversionDispatcher', () => {
+	afterEach( () => {
+		log.warn = logWarn;
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'should create ViewConversionDispatcher with passed api', () => {
 			const apiObj = {};
@@ -34,6 +42,8 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should fire viewCleanup event on converted view part', () => {
+			silenceWarnings();
+
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
@@ -43,6 +53,8 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should fire proper events', () => {
+			silenceWarnings();
+
 			const viewText = new ViewText( 'foobar' );
 			const viewElement = new ViewContainerElement( 'p', null, viewText );
 			const viewFragment = new ViewDocumentFragment( viewElement );
@@ -59,15 +71,17 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should convert ViewText', () => {
+			const spy = sinon.spy();
 			const viewText = new ViewText( 'foobar' );
 
 			dispatcher.on( 'text', ( evt, data, consumable, conversionApi ) => {
-				const result = {
-					eventName: evt.name,
-					input: data.input,
-					// Check whether additional data has been passed.
-					foo: data.foo
-				};
+				// Check if this method has been fired.
+				spy();
+
+				// Check correctness of passed parameters.
+				expect( evt.name ).to.equal( 'text' );
+				expect( data.input ).to.equal( viewText );
+				expect( data.foo ).to.equal( 'bar' );
 
 				// Check whether consumable has appropriate value to consume.
 				expect( consumable.consume( data.input ) ).to.be.true;
@@ -77,30 +91,31 @@ describe( 'ViewConversionDispatcher', () => {
 
 				// Set conversion result to `output` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = result;
+				data.output = new ModelText( data.foo );
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
 			const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( conversionResult ).to.deep.equal( {
-				eventName: 'text',
-				input: viewText,
-				foo: 'bar'
-			} );
+			// Result should be wrapped in document fragment.
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'bar' );
+			expect( spy.calledOnce ).to.be.true;
 		} );
 
 		it( 'should convert ViewContainerElement', () => {
+			const spy = sinon.spy();
 			const viewElement = new ViewContainerElement( 'p', { attrKey: 'attrValue' } );
 
 			dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
-				const result = {
-					eventName: evt.name,
-					input: data.input,
-					// Check whether additional data has been passed.
-					foo: data.foo
-				};
+				// Check if this method has been fired.
+				spy();
+
+				// Check correctness of passed parameters.
+				expect( evt.name ).to.equal( 'element:p' );
+				expect( data.input ).to.equal( viewElement );
+				expect( data.foo ).to.equal( 'bar' );
 
 				// Check whether consumable has appropriate value to consume.
 				expect( consumable.consume( data.input, { name: true } ) ).to.be.true;
@@ -111,30 +126,31 @@ describe( 'ViewConversionDispatcher', () => {
 
 				// Set conversion result to `output` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = result;
+				data.output = new ModelElement( 'paragraph' );
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
 			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( conversionResult ).to.deep.equal( {
-				eventName: 'element:p',
-				input: viewElement,
-				foo: 'bar'
-			} );
+			// Result should be wrapped in document fragment.
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ).name ).to.equal( 'paragraph' );
+			expect( spy.calledOnce ).to.be.true;
 		} );
 
 		it( 'should convert ViewDocumentFragment', () => {
+			const spy = sinon.spy();
 			const viewFragment = new ViewDocumentFragment();
 
 			dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
-				const result = {
-					eventName: evt.name,
-					input: data.input,
-					// Check whether additional data has been passed.
-					foo: data.foo
-				};
+				// Check if this method has been fired.
+				spy();
+
+				// Check correctness of passed parameters.
+				expect( evt.name ).to.equal( 'documentFragment' );
+				expect( data.input ).to.equal( viewFragment );
+				expect( data.foo ).to.equal( 'bar' );
 
 				// Check whether consumable has appropriate value to consume.
 				expect( consumable.consume( data.input ) ).to.be.true;
@@ -144,44 +160,16 @@ describe( 'ViewConversionDispatcher', () => {
 
 				// Set conversion result to `output` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = result;
+				data.output = new ModelDocumentFragment( [ new ModelText( 'foo' ) ] );
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
 			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( conversionResult ).to.deep.equal( {
-				eventName: 'documentFragment',
-				input: viewFragment,
-				foo: 'bar'
-			} );
-		} );
-
-		it( 'should always wrap converted element by ModelDocumentFragment', () => {
-			const viewElement = new ViewContainerElement( 'p' );
-
-			dispatcher.on( 'element', ( evt, data ) => {
-				data.output = new ModelElement( 'paragraph' );
-			} );
-
-			const documentFragment = dispatcher.convert( viewElement, { foo: 'bar' } );
-
-			expect( documentFragment ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( documentFragment ) ).to.equal( '<paragraph></paragraph>' );
-		} );
-
-		it( 'should not wrap ModelDocumentFragment', () => {
-			const viewFragment = new ViewDocumentFragment();
-
-			dispatcher.on( 'documentFragment', ( evt, data ) => {
-				data.output = new ModelDocumentFragment();
-			} );
-
-			const documentFragment = dispatcher.convert( viewFragment );
-
-			expect( documentFragment ).to.instanceof( ModelDocumentFragment );
-			expect( documentFragment.childCount ).to.equal( 0 );
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foo' );
+			expect( spy.calledOnce ).to.be.true;
 		} );
 
 		it( 'should extract temporary markers stamps from converter element and create static markers list', () => {
@@ -207,91 +195,165 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 	} );
 
-	describe( 'conversionApi#convertItem', () => {
-		it( 'should convert view elements and view text', () => {
-			const dispatcher = new ViewConversionDispatcher();
-			const viewFragment = new ViewDocumentFragment( [
-				new ViewContainerElement( 'p' ), new ViewText( 'foobar' )
-			] );
+	describe( 'conversionApi', () => {
+		let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher;
+		let spyNull, spyArray, viewDiv, viewNull, viewArray;
+
+		beforeEach( () => {
+			spy = sinon.spy();
+			spyP = sinon.spy();
+			spyText = sinon.spy();
+
+			viewP = new ViewContainerElement( 'p' );
+			viewText = new ViewText( 'foobar' );
+			modelP = new ModelElement( 'paragraph' );
+			modelText = new ModelText( 'foobar' );
+
+			consumableMock = {};
+
+			dispatcher = new ViewConversionDispatcher();
+
+			dispatcher.on( 'element:p', ( evt, data, consumable ) => {
+				spyP();
 
-			dispatcher.on( 'text', ( evt, data ) => {
-				data.output = { text: data.input.data };
+				expect( data.foo ).to.equal( 'bar' );
+				expect( consumable ).to.equal( consumableMock );
+
+				data.output = modelP;
 			} );
 
-			dispatcher.on( 'element:p', ( evt, data ) => {
-				data.output = { name: 'p' };
+			dispatcher.on( 'text', ( evt, data, consumable ) => {
+				spyText();
+
+				expect( data.foo ).to.equal( 'bar' );
+				expect( consumable ).to.equal( consumableMock );
+
+				data.output = modelText;
 			} );
 
-			dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
-				data.output = [];
+			spyNull = sinon.spy();
+			spyArray = sinon.spy();
+
+			viewDiv = new ViewContainerElement( 'div' ); // Will not be recognized and not converted.
+			viewNull = new ViewContainerElement( 'null' ); // Will return `null` in `data.output` upon conversion.
+			viewArray = new ViewContainerElement( 'array' ); // Will return an array in `data.output` upon conversion.
 
-				for ( let child of data.input.getChildren() ) {
-					data.output.push( conversionApi.convertItem( child ) );
-				}
+			dispatcher.on( 'element:null', ( evt, data ) => {
+				spyNull();
+
+				data.output = null;
 			} );
 
-			expect( dispatcher.convert( viewFragment ) ).to.deep.equal( [
-				{ name: 'p' },
-				{ text: 'foobar' }
-			] );
+			dispatcher.on( 'element:array', ( evt, data ) => {
+				spyArray();
+
+				data.output = [ new ModelText( 'foo' ) ];
+			} );
 		} );
-	} );
 
-	describe( 'conversionApi#convertChildren', () => {
-		it( 'should fire proper events for all children of passed view part', () => {
-			const dispatcher = new ViewConversionDispatcher();
-			const viewFragment = new ViewDocumentFragment( [
-				new ViewContainerElement( 'p' ), new ViewText( 'foobar' )
-			] );
+		describe( 'convertItem', () => {
+			it( 'should pass consumable and additional data to proper converter and return data.output', () => {
+				silenceWarnings();
 
-			dispatcher.on( 'text', ( evt, data ) => {
-				data.output = { text: data.input.data };
-			} );
+				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+					spy();
+
+					expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP );
+					expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText );
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } );
 
-			dispatcher.on( 'element:p', ( evt, data ) => {
-				data.output = { name: 'p' };
+				expect( spy.calledOnce ).to.be.true;
+				expect( spyP.calledOnce ).to.be.true;
+				expect( spyText.calledOnce ).to.be.true;
 			} );
 
-			dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
-				data.output = conversionApi.convertChildren( data.input );
+			it( 'should return null if element was not converted and log a warning', () => {
+				sinon.spy( log, 'warn' );
+
+				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+					spy();
+
+					expect( conversionApi.convertItem( viewDiv ) ).to.equal( null );
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment() );
+
+				expect( spy.calledOnce ).to.be.true;
+				expect( log.warn.calledOnce );
+
+				log.warn.restore();
 			} );
 
-			expect( dispatcher.convert( viewFragment ) ).to.deep.equal( [
-				{ name: 'p' },
-				{ text: 'foobar' }
-			] );
-		} );
+			it( 'should return null if element was incorrectly converted and log a warning', () => {
+				sinon.spy( log, 'warn' );
+
+				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+					spy();
+
+					expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
+					expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
+				} );
 
-		it( 'should flatten structure of non-converted elements', () => {
-			const dispatcher = new ViewConversionDispatcher();
+				dispatcher.convert( new ViewDocumentFragment() );
 
-			dispatcher.on( 'text', ( evt, data ) => {
-				data.output = data.input.data;
+				expect( spy.calledOnce ).to.be.true;
+				expect( spyArray.calledOnce ).to.be.true;
+				expect( spyNull.calledOnce ).to.be.true;
+				expect( log.warn.calledOnce );
+
+				log.warn.restore();
 			} );
+		} );
 
-			dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
-				data.output = conversionApi.convertChildren( data.input, consumable );
+		describe( 'convertChildren', () => {
+			it( 'should fire conversion for all children of passed element and return conversion results wrapped in document fragment', () => {
+				silenceWarnings();
+
+				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+					spy();
+
+					const result = conversionApi.convertChildren( data.input, consumableMock, data );
+
+					expect( result ).to.be.instanceof( ModelDocumentFragment );
+					expect( result.childCount ).to.equal( 2 );
+					expect( result.getChild( 0 ) ).to.equal( modelP );
+					expect( result.getChild( 1 ) ).to.equal( modelText );
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } );
+
+				expect( spy.calledOnce ).to.be.true;
+				expect( spyP.calledOnce ).to.be.true;
+				expect( spyText.calledOnce ).to.be.true;
 			} );
 
-			const viewStructure = new ViewContainerElement( 'div', null, [
-				new ViewContainerElement( 'p', null, [
-					new ViewContainerElement( 'span', { class: 'nice' }, [
-						new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ),
-						new ViewText( ' bar ' ),
-						new ViewAttributeElement( 'i', null, new ViewText( 'xyz' ) )
-					] )
-				] ),
-				new ViewContainerElement( 'p', null, [
-					new ViewAttributeElement( 'strong', null, [
-						new ViewText( 'aaa ' ),
-						new ViewAttributeElement( 'span', null, new ViewText( 'bbb' ) ),
-						new ViewText( ' ' ),
-						new ViewAttributeElement( 'a', { href: 'bar.html' }, new ViewText( 'ccc' ) )
-					] )
-				] )
-			] );
-
-			expect( dispatcher.convert( viewStructure ) ).to.deep.equal( [ 'foo', ' bar ', 'xyz', 'aaa ', 'bbb', ' ', 'ccc' ] );
+			it( 'should filter out incorrectly converted elements and log warnings', () => {
+				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+					spy();
+
+					const result = conversionApi.convertChildren( data.input, consumableMock, data );
+
+					expect( result ).to.be.instanceof( ModelDocumentFragment );
+					expect( result.childCount ).to.equal( 2 );
+					expect( result.getChild( 0 ) ).to.equal( modelP );
+					expect( result.getChild( 1 ) ).to.equal( modelText );
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } );
+
+				expect( spy.calledOnce ).to.be.true;
+				expect( spyNull.calledOnce ).to.be.true;
+				expect( spyArray.calledOnce ).to.be.true;
+				expect( log.warn.calledThirce );
+			} );
 		} );
 	} );
+
+	// Silences warnings that pop up in tests. Use when the test checks a specific functionality and we are not interested in those logs.
+	// No need to restore `log.warn` - it is done in `afterEach()`.
+	function silenceWarnings() {
+		log.warn = () => {};
+	}
 } );