diff --git a/src/conversion/viewconversiondispatcher.js b/src/conversion/viewconversiondispatcher.js index bd4b8fe59..8c3079f2f 100644 --- a/src/conversion/viewconversiondispatcher.js +++ b/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,35 @@ 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; + // We can get a null here if conversion failed (see _convertItem()) + // or simply if an item could not be converted (e.g. due to the schema). + 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; } /** @@ -180,6 +181,21 @@ export default class ViewConversionDispatcher { this.fire( 'documentFragment', data, consumable, this.conversionApi ); } + // Handle incorrect `data.output`. + if ( data.output && !( 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 ] ); + + return null; + } + return data.output; } @@ -188,11 +204,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 items that has not been converted or for which conversion returned wrong result (for those warning is logged). + // 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 +332,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} or `null` (as set by default). * * @method #convertItem * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element @@ -314,7 +344,8 @@ 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 {*} The result of item conversion, created and modified by callbacks attached to fired event. + * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment|null} The result of item conversion, + * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect. */ /** @@ -329,5 +360,6 @@ 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~DocumentFragment} Model document fragment containing results of conversion + * of all children of given item. */ diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js index 755c9fd1b..bb0a75cc1 100644 --- a/tests/conversion/advanced-converters.js +++ b/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++; } } diff --git a/tests/conversion/buildviewconverter.js b/tests/conversion/buildviewconverter.js index f166ff5f7..8f2f09b13 100644 --- a/tests/conversion/buildviewconverter.js +++ b/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( 'foo' ); } ); + 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', () => { diff --git a/tests/conversion/view-to-model-converters.js b/tests/conversion/view-to-model-converters.js index fc9a9bd80..fae127644 100644 --- a/tests/conversion/view-to-model-converters.js +++ b/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' ); } ); diff --git a/tests/conversion/viewconversiondispatcher.js b/tests/conversion/viewconversiondispatcher.js index 9c3a712f3..333b3a42a 100644 --- a/tests/conversion/viewconversiondispatcher.js +++ b/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( '' ); - } ); - - 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,169 @@ 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(); + + expect( data.foo ).to.equal( 'bar' ); + expect( consumable ).to.equal( consumableMock ); - dispatcher.on( 'text', ( evt, data ) => { - data.output = { text: data.input.data }; + 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. + + dispatcher.on( 'element:null', ( evt, data ) => { + spyNull(); - for ( let child of data.input.getChildren() ) { - data.output.push( conversionApi.convertItem( child ) ); - } + 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(); - dispatcher.on( 'element:p', ( evt, data ) => { - data.output = { name: 'p' }; + expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP ); + expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText ); + } ); + + dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } ); + + 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 do nothing if element was not converted', () => { + sinon.spy( log, 'warn' ); + + dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => { + spy(); + + expect( conversionApi.convertItem( viewDiv ) ).to.equal( null ); + expect( conversionApi.convertItem( viewNull ) ).to.equal( null ); + } ); + + dispatcher.convert( new ViewDocumentFragment() ); + + expect( spy.calledOnce ).to.be.true; + expect( spyNull.calledOnce ).to.be.true; + expect( log.warn.called ).to.be.false; + + 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' ); - it( 'should flatten structure of non-converted elements', () => { - const dispatcher = new ViewConversionDispatcher(); + dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => { + spy(); - dispatcher.on( 'text', ( evt, data ) => { - data.output = data.input.data; + expect( conversionApi.convertItem( viewArray ) ).to.equal( null ); + } ); + + dispatcher.convert( new ViewDocumentFragment() ); + + expect( spy.calledOnce ).to.be.true; + expect( spyArray.calledOnce ).to.be.true; + expect( log.warn.calledOnce ).to.be.true; + + 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', () => { + sinon.spy( log, 'warn' ); + + 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.calledOnce ).to.be.true; + + log.warn.restore(); + } ); } ); } ); + + // 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 = () => {}; + } } );