text: /**
   * @license Copyright (c) 2003-2022, CKSource - Frederico Knabben. All rights reserved.
   * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
   */
  
  import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
  import Mapper from '../../src/conversion/mapper';
  
  import Model from '../../src/model/model';
  import ModelText from '../../src/model/text';
  import ModelElement from '../../src/model/element';
  import ModelDocumentFragment from '../../src/model/documentfragment';
  import ModelRange from '../../src/model/range';
  
  import View from '../../src/view/view';
  import ViewContainerElement from '../../src/view/containerelement';
  import { StylesProcessor } from '../../src/view/stylesmap';
  
  describe( 'DowncastDispatcher', () => {
  	let dispatcher, doc, root, differStub, model, view, mapper;
  
  	beforeEach( () => {
  		model = new Model();
  		view = new View( new StylesProcessor() );
  		doc = model.document;
  		mapper = new Mapper();
  		dispatcher = new DowncastDispatcher( { mapper } );
  		root = doc.createRoot();
  
  		differStub = {
  			getMarkersToRemove: () => [],
  			getChanges: () => [],
  			getMarkersToAdd: () => []
  		};
  	} );
  
  	describe( 'constructor()', () => {
  		it( 'should create DowncastDispatcher with given api', () => {
  			const apiObj = {};
  			const dispatcher = new DowncastDispatcher( { apiObj } );
  
  			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
  		} );
  	} );
  
  	describe( 'convertChanges', () => {
  		it( 'should call convertInsert for insert change', () => {
  			sinon.stub( dispatcher, 'convertInsert' );
  
  			const position = model.createPositionFromPath( root, [ 0 ] );
  			const range = ModelRange._createFromPositionAndShift( position, 1 );
  
  			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertInsert.calledOnce ).to.be.true;
  			expect( dispatcher.convertInsert.firstCall.args[ 0 ].isEqual( range ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should call convertRemove for remove change', () => {
  			sinon.stub( dispatcher, 'convertRemove' );
  
  			const position = model.createPositionFromPath( root, [ 0 ] );
  
  			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should call convertAttribute for attribute change', () => {
  			sinon.stub( dispatcher, 'convertAttribute' );
  			sinon.stub( dispatcher, '_mapChangesWithAutomaticReconversion' ).callsFake( differ => differ.getChanges() );
  
  			const position = model.createPositionFromPath( root, [ 0 ] );
  			const range = ModelRange._createFromPositionAndShift( position, 1 );
  
  			differStub.getChanges = () => [
  				{ type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' }
  			];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should handle multiple changes', () => {
  			sinon.stub( dispatcher, 'convertInsert' );
  			sinon.stub( dispatcher, 'convertRemove' );
  			sinon.stub( dispatcher, 'convertAttribute' );
  			sinon.stub( dispatcher, '_mapChangesWithAutomaticReconversion' ).callsFake( differ => differ.getChanges() );
  
  			const position = model.createPositionFromPath( root, [ 0 ] );
  			const range = ModelRange._createFromPositionAndShift( position, 1 );
  
  			differStub.getChanges = () => [
  				{ type: 'insert', position, length: 1 },
  				{ type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' },
  				{ type: 'remove', position, length: 1, name: 'paragraph' },
  				{ type: 'insert', position, length: 3 }
  			];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
  			expect( dispatcher.convertRemove.calledOnce ).to.be.true;
  			expect( dispatcher.convertAttribute.calledOnce ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should call convertMarkerAdd when markers are added', () => {
  			sinon.stub( dispatcher, 'convertMarkerAdd' );
  
  			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  
  			differStub.getMarkersToAdd = () => [
  				{ name: 'foo', range: fooRange },
  				{ name: 'bar', range: barRange }
  			];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
  			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should call convertMarkerRemove when markers are removed', () => {
  			sinon.stub( dispatcher, 'convertMarkerRemove' );
  
  			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  
  			differStub.getMarkersToRemove = () => [
  				{ name: 'foo', range: fooRange },
  				{ name: 'bar', range: barRange }
  			];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
  			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should re-render markers which view elements got unbound during conversion', () => {
  			sinon.stub( dispatcher, 'convertMarkerRemove' );
  			sinon.stub( dispatcher, 'convertMarkerAdd' );
  
  			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  
  			model.markers._set( 'foo', fooRange );
  			model.markers._set( 'bar', barRange );
  
  			// Stub `Mapper#flushUnboundMarkerNames`.
  			dispatcher.conversionApi.mapper.flushUnboundMarkerNames = () => [ 'foo', 'bar' ];
  
  			view.change( writer => {
  				dispatcher.convertChanges( differStub, model.markers, writer );
  			} );
  
  			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
  			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
  			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
  			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  
  	describe( 'convertInsert', () => {
  		it( 'should fire event with correct parameters for every item in passed range', () => {
  			root._appendChild( [
  				new ModelText( 'foo', { bold: true } ),
  				new ModelElement( 'imageBlock' ),
  				new ModelText( 'bar' ),
  				new ModelElement( 'paragraph', { class: 'nice' }, new ModelText( 'xx', { italic: true } ) )
  			] );
  
  			const range = model.createRangeIn( root );
  			const loggedEvents = [];
  
  			// We will check everything connected with insert event:
  			dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
  				// Check if the item is correct.
  				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
  				// Check if the range is correct.
  				const log = 'insert:' + itemId + ':' + data.range.start.path + ':' + data.range.end.path;
  
  				loggedEvents.push( log );
  
  				// Check if the event name is correct.
  				expect( evt.name ).to.equal( 'insert:' + ( data.item.name || '$text' ) );
  				// Check if model consumable is correct.
  				expect( conversionApi.consumable.consume( data.item, 'insert' ) ).to.be.true;
  			} );
  
  			// Same here.
  			dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
  				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
  				const key = data.attributeKey;
  				const value = data.attributeNewValue;
  				const log = 'attribute:' + key + ':' + value + ':' + itemId + ':' + data.range.start.path + ':' + data.range.end.path;
  
  				loggedEvents.push( log );
  
  				expect( evt.name ).to.equal( 'attribute:' + key + ':' + ( data.item.name || '$text' ) );
  				expect( conversionApi.consumable.consume( data.item, 'attribute:' + key ) ).to.be.true;
  			} );
  
  			dispatcher.convertInsert( range );
  
  			// Check the data passed to called events and the order of them.
  			expect( loggedEvents ).to.deep.equal( [
  				'insert:$text:foo:0:3',
  				'attribute:bold:true:$text:foo:0:3',
  				'insert:imageBlock:3:4',
  				'insert:$text:bar:4:7',
  				'insert:paragraph:7:8',
  				'attribute:class:nice:paragraph:7:8',
  				'insert:$text:xx:7,0:7,2',
  				'attribute:italic:true:$text:xx:7,0:7,2'
  			] );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire events for already consumed parts of model', () => {
  			root._appendChild( [
  				new ModelElement( 'imageBlock', { src: 'foo.jpg', title: 'bar', bold: true }, [
  					new ModelElement( 'caption', {}, new ModelText( 'title' ) )
  				] )
  			] );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.on( 'insert:imageBlock', ( evt, data, conversionApi ) => {
  				conversionApi.consumable.consume( data.item.getChild( 0 ), 'insert' );
  				conversionApi.consumable.consume( data.item, 'attribute:bold' );
  			} );
  
  			const range = model.createRangeIn( root );
  
  			dispatcher.convertInsert( range );
  
  			expect( dispatcher.fire.calledWith( 'insert:imageBlock' ) ).to.be.true;
  			expect( dispatcher.fire.calledWith( 'attribute:src:imageBlock' ) ).to.be.true;
  			expect( dispatcher.fire.calledWith( 'attribute:title:imageBlock' ) ).to.be.true;
  			expect( dispatcher.fire.calledWith( 'insert:$text' ) ).to.be.true;
  
  			expect( dispatcher.fire.calledWith( 'attribute:bold:imageBlock' ) ).to.be.false;
  			expect( dispatcher.fire.calledWith( 'insert:caption' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  
  	describe( 'convertRemove', () => {
  		it( 'should fire event for removed range', () => {
  			const loggedEvents = [];
  
  			dispatcher.on( 'remove:$text', ( evt, data ) => {
  				const log = 'remove:' + data.position.path + ':' + data.length;
  				loggedEvents.push( log );
  			} );
  
  			dispatcher.convertRemove( model.createPositionAt( root, 3 ), 3, '$text' );
  
  			expect( loggedEvents ).to.deep.equal( [ 'remove:3:3' ] );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  
  	describe( 'convertSelection', () => {
  		beforeEach( () => {
  			dispatcher.off( 'selection' );
  
  			root._appendChild( new ModelText( 'foobar' ) );
  			model.change( writer => {
  				writer.setSelection( [
  					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 3 ] ) ),
  					writer.createRange( writer.createPositionFromPath( root, [ 4 ] ), writer.createPositionFromPath( root, [ 5 ] ) )
  				] );
  			} );
  		} );
  
  		it( 'should fire selection event', () => {
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertSelection( doc.selection, model.markers, [] );
  
  			expect( dispatcher.fire.calledWith(
  				'selection',
  				{ selection: sinon.match.instanceOf( doc.selection.constructor ) }
  			) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should prepare correct list of consumable values', () => {
  			model.change( writer => {
  				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  				writer.setAttribute( 'italic', true,
  					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  				);
  			} );
  
  			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  				expect( conversionApi.consumable.test( data.selection, 'selection' ) ).to.be.true;
  				expect( conversionApi.consumable.test( data.selection, 'attribute:bold' ) ).to.be.true;
  				expect( conversionApi.consumable.test( data.selection, 'attribute:italic' ) ).to.be.null;
  			} );
  
  			dispatcher.convertSelection( doc.selection, model.markers, [] );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire attributes events for non-collapsed selection', () => {
  			model.change( writer => {
  				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  				writer.setAttribute( 'italic', true,
  					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  				);
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertSelection( doc.selection, model.markers, [] );
  
  			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
  			expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should fire attributes events for collapsed selection', () => {
  			model.change( writer => {
  				writer.setSelection(
  					writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
  				);
  			} );
  
  			model.change( writer => {
  				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertSelection( doc.selection, model.markers, [] );
  
  			expect( dispatcher.fire.calledWith( 'attribute:bold:$text' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire attributes events if attribute has been consumed', () => {
  			model.change( writer => {
  				writer.setSelection(
  					writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
  				);
  			} );
  
  			model.change( writer => {
  				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  				writer.setAttribute( 'italic', true,
  					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  				);
  			} );
  
  			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  				conversionApi.consumable.consume( data.selection, 'attribute:bold' );
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertSelection( doc.selection, model.markers, [] );
  
  			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should fire events for markers for collapsed selection', () => {
  			model.change( writer => {
  				writer.setSelection(
  					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
  				);
  				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  				writer.addMarker( 'name', { range, usingOperation: false } );
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  			dispatcher.convertSelection( doc.selection, model.markers, markers );
  
  			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should fire events for all markers of the same group for collapsed selection', () => {
  			model.change( writer => {
  				writer.setSelection(
  					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
  				);
  				writer.addMarker( 'name:1', {
  					range: writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) ),
  					usingOperation: false
  				} );
  				writer.addMarker( 'name:2', {
  					range: writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 3 ) ),
  					usingOperation: false
  				} );
  			} );
  
  			dispatcher.on( 'addMarker', ( evt, data, { consumable } ) => consumable.consume( data.item, evt.name ) );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  			dispatcher.convertSelection( doc.selection, model.markers, markers );
  
  			expect( dispatcher.fire.calledWith( 'addMarker:name:1' ) ).to.be.true;
  			expect( dispatcher.fire.calledWith( 'addMarker:name:2' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire events for markers for non-collapsed selection', () => {
  			model.change( writer => {
  				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  				writer.addMarker( 'name', { range, usingOperation: false } );
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  			dispatcher.convertSelection( doc.selection, model.markers, markers );
  
  			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire event for marker if selection is in a element with custom highlight handling', () => {
  			// Clear after `beforeEach`.
  			root._removeChildren( 0, root.childCount );
  
  			const text = new ModelText( 'abc' );
  			const caption = new ModelElement( 'caption', null, text );
  			const image = new ModelElement( 'imageBlock', null, caption );
  			root._appendChild( [ image ] );
  
  			// Create view elements that will be "mapped" to model elements.
  			const viewCaption = new ViewContainerElement( view.document, 'caption' );
  			const viewFigure = new ViewContainerElement( view.document, 'figure', null, viewCaption );
  
  			// Create custom highlight handler mock.
  			viewFigure._setCustomProperty( 'addHighlight', () => {} );
  			viewFigure._setCustomProperty( 'removeHighlight', () => {} );
  
  			// Create mapper mock.
  			dispatcher.conversionApi.mapper = {
  				toViewElement( modelElement ) {
  					if ( modelElement == image ) {
  						return viewFigure;
  					} else if ( modelElement == caption ) {
  						return viewCaption;
  					}
  				}
  			};
  
  			model.change( writer => {
  				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 1 ) );
  				writer.addMarker( 'name', { range, usingOperation: false } );
  				writer.setSelection( caption, 1 );
  			} );
  			sinon.spy( dispatcher, 'fire' );
  
  			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  
  			dispatcher.convertSelection( doc.selection, model.markers, markers );
  
  			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire events if information about marker has been consumed', () => {
  			model.change( writer => {
  				writer.setSelection(
  					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
  				);
  
  				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  				writer.addMarker( 'foo', { range, usingOperation: false } );
  				writer.addMarker( 'bar', { range, usingOperation: false } );
  			} );
  
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.on( 'addMarker:foo', ( evt, data, conversionApi ) => {
  				conversionApi.consumable.consume( data.item, 'addMarker:bar' );
  			} );
  
  			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  			dispatcher.convertSelection( doc.selection, model.markers, markers );
  
  			expect( dispatcher.fire.calledWith( 'addMarker:foo' ) ).to.be.true;
  			expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  
  	describe( 'convertMarkerAdd', () => {
  		let element, text;
  
  		beforeEach( () => {
  			text = new ModelText( 'foo bar baz' );
  			element = new ModelElement( 'paragraph', null, [ text ] );
  			root._appendChild( [ element ] );
  		} );
  
  		it( 'should fire addMarker event for whole collapsed marker', () => {
  			const range = model.createRange( model.createPositionAt( element, 2 ), model.createPositionAt( element, 2 ) );
  
  			const spy = sinon.spy();
  
  			dispatcher.on( 'addMarker:name', ( evt, data ) => {
  				spy();
  
  				expect( data.markerName ).to.equal( 'name' );
  				expect( data.markerRange.isEqual( range ) ).to.be.true;
  			} );
  
  			dispatcher.convertMarkerAdd( 'name', range );
  
  			expect( spy.calledOnce ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should convert marker in document fragment', () => {
  			const text = new ModelText( 'foo' );
  			const docFrag = new ModelDocumentFragment( text );
  			const eleRange = model.createRange( model.createPositionAt( docFrag, 1 ), model.createPositionAt( docFrag, 2 ) );
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertMarkerAdd( 'name', eleRange );
  
  			expect( dispatcher.fire.called ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not convert marker if it is in graveyard', () => {
  			const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertMarkerAdd( 'name', gyRange );
  
  			expect( dispatcher.fire.called ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should fire addMarker event for whole non-collapsed marker and for each item in the range', () => {
  			const range = model.createRangeIn( root );
  
  			const spyWholeRange = sinon.spy();
  
  			dispatcher.on( 'addMarker:name', ( evt, data ) => {
  				if ( !data.item ) {
  					spyWholeRange();
  
  					expect( data.markerName ).to.equal( 'name' );
  					expect( data.markerRange.isEqual( range ) ).to.be.true;
  				}
  			} );
  
  			const spyItems = sinon.spy();
  			const items = [];
  
  			dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
  				if ( data.item ) {
  					spyItems();
  
  					expect( data.markerName ).to.equal( 'name' );
  					expect( data.markerRange.isEqual( range ) ).to.be.true;
  					expect( conversionApi.consumable.test( data.item, 'addMarker:name' ) );
  
  					items.push( data.item );
  				}
  			} );
  
  			dispatcher.convertMarkerAdd( 'name', range );
  
  			expect( spyWholeRange.calledOnce ).to.be.true;
  			expect( spyItems.calledTwice ).to.be.true;
  
  			expect( items[ 0 ] ).to.equal( element );
  			expect( items[ 1 ].data ).to.equal( text.data );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not fire conversion for non-collapsed marker items if marker was consumed in earlier event', () => {
  			const range = model.createRangeIn( root );
  
  			dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
  				if ( !data.item ) {
  					conversionApi.consumable.consume( data.markerRange, evt.name );
  				}
  			}, { priority: 'high' } );
  
  			const spyItems = sinon.spy();
  
  			dispatcher.on( 'addMarker:name', ( evt, data ) => {
  				if ( data.item ) {
  					spyItems();
  				}
  			} );
  
  			dispatcher.convertMarkerAdd( 'name', range );
  
  			expect( spyItems.called ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should be possible to override #1', () => {
  			const range = model.createRangeIn( root );
  
  			const addMarkerSpy = sinon.spy();
  			const highAddMarkerSpy = sinon.spy();
  
  			dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  				if ( !data.item ) {
  					addMarkerSpy();
  				}
  			} );
  
  			dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  				if ( !data.item ) {
  					highAddMarkerSpy();
  
  					evt.stop();
  				}
  			}, { priority: 'high' } );
  
  			dispatcher.convertMarkerAdd( 'marker', range );
  
  			expect( addMarkerSpy.called ).to.be.false;
  			expect( highAddMarkerSpy.calledOnce ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should be possible to override #2', () => {
  			const range = model.createRangeIn( root );
  
  			const addMarkerSpy = sinon.spy();
  			const highAddMarkerSpy = sinon.spy();
  
  			dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  				if ( data.item ) {
  					addMarkerSpy();
  				}
  			} );
  
  			dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  				if ( data.item ) {
  					highAddMarkerSpy();
  
  					evt.stop();
  				}
  			}, { priority: 'high' } );
  
  			dispatcher.convertMarkerAdd( 'marker', range );
  
  			expect( addMarkerSpy.called ).to.be.false;
  
  			// Called once for each item, twice total.
  			expect( highAddMarkerSpy.calledTwice ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  
  	describe( 'convertMarkerRemove', () => {
  		let range, element, text;
  
  		beforeEach( () => {
  			text = new ModelText( 'foo bar baz' );
  			element = new ModelElement( 'paragraph', null, [ text ] );
  			root._appendChild( [ element ] );
  
  			range = model.createRange( model.createPositionAt( element, 0 ), model.createPositionAt( element, 4 ) );
  		} );
  
  		it( 'should fire removeMarker event', () => {
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertMarkerRemove( 'name', range );
  
  			expect( dispatcher.fire.calledWith( 'removeMarker:name' ) ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should not convert marker if it is in graveyard', () => {
  			const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertMarkerRemove( 'name', gyRange );
  
  			expect( dispatcher.fire.called ).to.be.false;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should convert marker in document fragment', () => {
  			const text = new ModelText( 'foo' );
  			const docFrag = new ModelDocumentFragment( text );
  			const eleRange = model.createRange( model.createPositionAt( docFrag, 1 ), model.createPositionAt( docFrag, 2 ) );
  			sinon.spy( dispatcher, 'fire' );
  
  			dispatcher.convertMarkerRemove( 'name', eleRange );
  
  			expect( dispatcher.fire.called ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should fire conversion for the range', () => {
  			range = model.createRangeIn( root );
  
  			dispatcher.on( 'addMarker:name', ( evt, data ) => {
  				expect( data.markerName ).to.equal( 'name' );
  				expect( data.markerRange.isEqual( range ) ).to.be.true;
  			} );
  
  			dispatcher.convertMarkerRemove( 'name', range );
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  
  		it( 'should be possible to override', () => {
  			range = model.createRangeIn( root );
  
  			const removeMarkerSpy = sinon.spy();
  			const highRemoveMarkerSpy = sinon.spy();
  
  			dispatcher.on( 'removeMarker:marker', removeMarkerSpy );
  
  			dispatcher.on( 'removeMarker:marker', evt => {
  				highRemoveMarkerSpy();
  
  				evt.stop();
  			}, { priority: 'high' } );
  
  			dispatcher.convertMarkerRemove( 'marker', range );
  
  			expect( removeMarkerSpy.called ).to.be.false;
  			expect( highRemoveMarkerSpy.calledOnce ).to.be.true;
  
  			expect( dispatcher.conversionApi.writer ).to.be.undefined;
  			expect( dispatcher.conversionApi.consumable ).to.be.undefined;
  		} );
  	} );
  } );
  