viewconversiondispatcher.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
  6. import ViewContainerElement from '../../src/view/containerelement';
  7. import ViewDocumentFragment from '../../src/view/documentfragment';
  8. import ViewText from '../../src/view/text';
  9. import Model from '../../src/model/model';
  10. import ModelText from '../../src/model/text';
  11. import ModelTextProxy from '../../src/model/textproxy';
  12. import ModelElement from '../../src/model/element';
  13. import ModelDocumentFragment from '../../src/model/documentfragment';
  14. import ModelPosition from '../../src/model/position';
  15. import ModelRange from '../../src/model/range';
  16. import ModelWriter from '../../src/model/writer';
  17. import first from '@ckeditor/ckeditor5-utils/src/first';
  18. import log from '@ckeditor/ckeditor5-utils/src/log';
  19. // Stored in case it is silenced and has to be restored.
  20. const logWarn = log.warn;
  21. describe( 'ViewConversionDispatcher', () => {
  22. let model;
  23. beforeEach( () => {
  24. model = new Model();
  25. log.warn = logWarn;
  26. } );
  27. describe( 'constructor()', () => {
  28. it( 'should create ViewConversionDispatcher with passed api', () => {
  29. const apiObj = {};
  30. const dispatcher = new ViewConversionDispatcher( model, { apiObj } );
  31. expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
  32. expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function );
  33. expect( dispatcher.conversionApi ).to.have.property( 'convertChildren' ).that.is.instanceof( Function );
  34. expect( dispatcher.conversionApi ).to.have.property( 'splitToAllowedParent' ).that.is.instanceof( Function );
  35. } );
  36. } );
  37. describe( 'convert()', () => {
  38. let dispatcher;
  39. beforeEach( () => {
  40. dispatcher = new ViewConversionDispatcher( model );
  41. } );
  42. it( 'should fire viewCleanup event on converted view part', () => {
  43. silenceWarnings();
  44. sinon.spy( dispatcher, 'fire' );
  45. const viewP = new ViewContainerElement( 'p' );
  46. dispatcher.convert( viewP );
  47. expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true;
  48. } );
  49. it( 'should fire proper events', () => {
  50. silenceWarnings();
  51. const viewText = new ViewText( 'foobar' );
  52. const viewElement = new ViewContainerElement( 'p', null, viewText );
  53. const viewFragment = new ViewDocumentFragment( viewElement );
  54. sinon.spy( dispatcher, 'fire' );
  55. dispatcher.convert( viewText );
  56. dispatcher.convert( viewElement );
  57. dispatcher.convert( viewFragment );
  58. expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true;
  59. expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true;
  60. expect( dispatcher.fire.calledWith( 'documentFragment' ) ).to.be.true;
  61. } );
  62. it( 'should convert ViewText', () => {
  63. const spy = sinon.spy();
  64. const viewText = new ViewText( 'foobar' );
  65. dispatcher.on( 'text', ( evt, data, consumable, conversionApi ) => {
  66. // Check if this method has been fired.
  67. spy();
  68. // Check correctness of passed parameters.
  69. expect( evt.name ).to.equal( 'text' );
  70. expect( data.input ).to.equal( viewText );
  71. expect( data.position ).to.instanceof( ModelPosition );
  72. // Check whether consumable has appropriate value to consume.
  73. expect( consumable.consume( data.input ) ).to.be.true;
  74. // Check whether conversionApi of `dispatcher` has been passed.
  75. expect( conversionApi ).to.equal( dispatcher.conversionApi );
  76. const text = conversionApi.writer.createText( data.input.data );
  77. conversionApi.writer.insert( text, data.position );
  78. // Set conversion result to `output` property of `data`.
  79. // Later we will check if it was returned by `convert` method.
  80. data.output = ModelRange.createOn( text );
  81. } );
  82. const conversionResult = dispatcher.convert( viewText );
  83. // Check conversion result.
  84. // Result should be wrapped in document fragment.
  85. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
  86. expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
  87. expect( spy.calledOnce ).to.be.true;
  88. } );
  89. it( 'should convert ViewContainerElement', () => {
  90. const spy = sinon.spy();
  91. const viewElement = new ViewContainerElement( 'p', { attrKey: 'attrValue' } );
  92. dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
  93. // Check if this method has been fired.
  94. spy();
  95. // Check correctness of passed parameters.
  96. expect( evt.name ).to.equal( 'element:p' );
  97. expect( data.input ).to.equal( viewElement );
  98. expect( data.position ).to.instanceof( ModelPosition );
  99. // Check whether consumable has appropriate value to consume.
  100. expect( consumable.consume( data.input, { name: true } ) ).to.be.true;
  101. expect( consumable.consume( data.input, { attribute: 'attrKey' } ) ).to.be.true;
  102. // Check whether conversionApi of `dispatcher` has been passed.
  103. expect( conversionApi ).to.equal( dispatcher.conversionApi );
  104. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  105. conversionApi.writer.insert( paragraph, data.position );
  106. // Set conversion result to `output` property of `data`.
  107. // Later we will check if it was returned by `convert` method.
  108. data.output = ModelRange.createOn( paragraph );
  109. } );
  110. // Use `additionalData` parameter to check if it was passed to the event.
  111. const conversionResult = dispatcher.convert( viewElement );
  112. // Check conversion result.
  113. // Result should be wrapped in document fragment.
  114. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
  115. expect( conversionResult.getChild( 0 ).name ).to.equal( 'paragraph' );
  116. expect( spy.calledOnce ).to.be.true;
  117. } );
  118. it( 'should convert ViewDocumentFragment', () => {
  119. const spy = sinon.spy();
  120. const viewFragment = new ViewDocumentFragment();
  121. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  122. // Check if this method has been fired.
  123. spy();
  124. // Check correctness of passed parameters.
  125. expect( evt.name ).to.equal( 'documentFragment' );
  126. expect( data.input ).to.equal( viewFragment );
  127. expect( data.position ).to.instanceof( ModelPosition );
  128. // Check whether consumable has appropriate value to consume.
  129. expect( consumable.consume( data.input ) ).to.be.true;
  130. // Check whether conversionApi of `dispatcher` has been passed.
  131. expect( conversionApi ).to.equal( dispatcher.conversionApi );
  132. const text = conversionApi.writer.createText( 'foo' );
  133. conversionApi.writer.insert( text, data.position );
  134. // Set conversion result to `output` property of `data`.
  135. // Later we will check if it was returned by `convert` method.
  136. data.output = ModelRange.createOn( text );
  137. } );
  138. // Use `additionalData` parameter to check if it was passed to the event.
  139. const conversionResult = dispatcher.convert( viewFragment );
  140. // Check conversion result.
  141. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
  142. expect( conversionResult.getChild( 0 ).data ).to.equal( 'foo' );
  143. expect( spy.calledOnce ).to.be.true;
  144. } );
  145. it( 'should add contextual properties to conversion api', () => {
  146. const viewElement = new ViewContainerElement( 'p', null, new ViewText( 'foobar' ) );
  147. // To be sure that both converters was called.
  148. const spy = sinon.spy();
  149. // To check that the same batch is used across conversion.
  150. let batch;
  151. // Contextual properties of ConversionApi should be undefined/empty before conversion.
  152. expect( dispatcher.conversionApi.writer ).to.not.ok;
  153. expect( dispatcher.conversionApi.data ).to.not.ok;
  154. expect( dispatcher._splitElements.size ).to.equal( 0 );
  155. dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
  156. // Check conversion api params.
  157. expect( conversionApi.writer ).to.instanceof( ModelWriter );
  158. expect( dispatcher._splitElements ).to.instanceof( Set );
  159. expect( dispatcher._splitElements.size ).to.equal( 0 );
  160. expect( conversionApi.data ).to.deep.equal( {} );
  161. // Remember writer batch to check in next converter that is exactly the same batch.
  162. batch = conversionApi.writer.batch;
  163. // Add some data to conversion API to verify them in next converter.
  164. // Set some custom data to conversion api data object.
  165. conversionApi.data.foo = 'bar';
  166. // Do the conversion.
  167. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  168. conversionApi.writer.insert( paragraph, data.position );
  169. // Add empty element and mark as a split result to check in next converter.
  170. const splitElement = conversionApi.writer.createElement( 'paragraph' );
  171. dispatcher._splitElements.add( splitElement );
  172. conversionApi.writer.insert( splitElement, ModelPosition.createAfter( paragraph ) );
  173. // Convert children - this wil call second converter.
  174. conversionApi.convertChildren( data.input, consumable, ModelPosition.createAt( paragraph ) );
  175. data.output = ModelRange.createOn( paragraph );
  176. spy();
  177. } );
  178. dispatcher.on( 'text', ( evt, data, consumable, conversionApi ) => {
  179. // The same batch is used in converters during one conversion.
  180. expect( conversionApi.writer.batch ).to.equal( batch );
  181. // Data set by previous converter are remembered.
  182. expect( conversionApi.data ).to.deep.equal( { foo: 'bar' } );
  183. // Split element is remembered as well.
  184. expect( dispatcher._splitElements.size ).to.equal( 1 );
  185. spy();
  186. } );
  187. dispatcher.convert( viewElement );
  188. // To be sure that both converters was called.
  189. sinon.assert.calledTwice( spy );
  190. // Contextual properties of ConversionApi should be cleared after conversion.
  191. expect( dispatcher.conversionApi.writer ).to.not.ok;
  192. expect( dispatcher.conversionApi.data ).to.not.ok;
  193. expect( dispatcher._splitElements.size ).to.equal( 0 );
  194. } );
  195. it( 'should remove empty elements that was created as a result of split', () => {
  196. const viewElement = new ViewContainerElement( 'p' );
  197. // To be sure that converter was called.
  198. const spy = sinon.spy();
  199. dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
  200. // First let's convert target element.
  201. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  202. conversionApi.writer.insert( paragraph, data.position );
  203. // Then add some elements and mark as split.
  204. // Create and insert empty split element before target element.
  205. const emptySplit = conversionApi.writer.createElement( 'paragraph' );
  206. conversionApi.writer.insert( emptySplit, ModelPosition.createAfter( paragraph ) );
  207. // Create and insert not empty split after target element.
  208. const notEmptySplit = conversionApi.writer.createElement( 'paragraph' );
  209. conversionApi.writer.appendText( 'foo', notEmptySplit );
  210. conversionApi.writer.insert( notEmptySplit, ModelPosition.createAfter( emptySplit ) );
  211. // Create and insert split with other split inside (both should be removed)
  212. const outerSplit = conversionApi.writer.createElement( 'paragraph' );
  213. const innerSplit = conversionApi.writer.createElement( 'paragraph' );
  214. conversionApi.writer.append( innerSplit, outerSplit );
  215. conversionApi.writer.insert( outerSplit, ModelPosition.createBefore( paragraph ) );
  216. dispatcher._splitElements.add( emptySplit );
  217. dispatcher._splitElements.add( notEmptySplit );
  218. dispatcher._splitElements.add( outerSplit );
  219. dispatcher._splitElements.add( innerSplit );
  220. data.output = ModelRange.createOn( paragraph );
  221. // We have the following result:
  222. // <p><p></p></p>[<p></p>]<p></p><p>foo</p>
  223. // Everything out of selected range is a result of the split.
  224. spy();
  225. } );
  226. const result = dispatcher.convert( viewElement );
  227. // Empty split elements should be removed and we should have the following result:
  228. // [<p></p>]<p>foo</p>
  229. expect( result.childCount ).to.equal( 2 );
  230. expect( result.getChild( 0 ).name ).to.equal( 'paragraph' );
  231. expect( result.getChild( 0 ).childCount ).to.equal( 0 );
  232. expect( result.getChild( 1 ).name ).to.equal( 'paragraph' );
  233. expect( result.getChild( 1 ).childCount ).to.equal( 1 );
  234. expect( result.getChild( 1 ).getChild( 0 ).data ).to.equal( 'foo' );
  235. } );
  236. it( 'should extract temporary markers elements from converter element and create static markers list', () => {
  237. const viewFragment = new ViewDocumentFragment();
  238. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  239. const fragment = new ModelDocumentFragment( [
  240. new ModelText( 'fo' ),
  241. new ModelElement( '$marker', { 'data-name': 'marker1' } ),
  242. new ModelText( 'o' ),
  243. new ModelElement( '$marker', { 'data-name': 'marker2' } ),
  244. new ModelText( 'b' ),
  245. new ModelElement( '$marker', { 'data-name': 'marker1' } ),
  246. new ModelText( 'ar' ),
  247. ] );
  248. conversionApi.writer.insert( fragment, data.position );
  249. data.output = ModelRange.createIn( data.position.parent );
  250. } );
  251. const conversionResult = dispatcher.convert( viewFragment );
  252. expect( conversionResult.markers.size ).to.equal( 2 );
  253. const marker1 = conversionResult.markers.get( 'marker1' );
  254. const marker2 = conversionResult.markers.get( 'marker2' );
  255. expect( marker1.start.path ).to.deep.equal( [ 2 ] );
  256. expect( marker1.end.path ).to.deep.equal( [ 4 ] );
  257. expect( marker2.start.path ).to.deep.equal( marker2.end.path ).to.deep.equal( [ 3 ] );
  258. } );
  259. } );
  260. describe( 'conversionApi', () => {
  261. let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, rootMock, dispatcher,
  262. spyNull, spyArray, viewDiv, viewNull, viewArray;
  263. beforeEach( () => {
  264. spy = sinon.spy();
  265. spyP = sinon.spy();
  266. spyText = sinon.spy();
  267. viewP = new ViewContainerElement( 'p' );
  268. viewText = new ViewText( 'foobar' );
  269. modelP = new ModelElement( 'paragraph' );
  270. modelText = new ModelText( 'foobar' );
  271. // Put nodes to documentFragment, this will mock root element and makes possible to create range on them.
  272. rootMock = new ModelDocumentFragment( [ modelP, modelText ] );
  273. consumableMock = {};
  274. dispatcher = new ViewConversionDispatcher( model, { schema: model.schema } );
  275. dispatcher.on( 'element:p', ( evt, data, consumable ) => {
  276. spyP();
  277. expect( consumable ).to.equal( consumableMock );
  278. data.output = ModelRange.createOn( modelP );
  279. } );
  280. dispatcher.on( 'text', ( evt, data, consumable ) => {
  281. spyText();
  282. expect( consumable ).to.equal( consumableMock );
  283. data.output = ModelRange.createOn( modelText );
  284. } );
  285. spyNull = sinon.spy();
  286. spyArray = sinon.spy();
  287. viewDiv = new ViewContainerElement( 'div' ); // Will not be recognized and not converted.
  288. viewNull = new ViewContainerElement( 'null' ); // Will return `null` in `data.output` upon conversion.
  289. viewArray = new ViewContainerElement( 'array' ); // Will return an array in `data.output` upon conversion.
  290. dispatcher.on( 'element:null', ( evt, data ) => {
  291. spyNull();
  292. data.output = null;
  293. } );
  294. dispatcher.on( 'element:array', ( evt, data ) => {
  295. spyArray();
  296. data.output = [ new ModelText( 'foo' ) ];
  297. } );
  298. } );
  299. describe( 'convertItem()', () => {
  300. it( 'should pass consumable and additional data to proper converter and return data.output', () => {
  301. silenceWarnings();
  302. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  303. spy();
  304. const result1 = conversionApi.convertItem( viewP, consumableMock, data.position );
  305. expect( result1 ).instanceof( ModelRange );
  306. expect( result1.start.path ).to.deep.equal( [ 0 ] );
  307. expect( result1.end.path ).to.deep.equal( [ 1 ] );
  308. expect( first( result1.getItems() ) ).to.equal( modelP );
  309. const result2 = conversionApi.convertItem( viewText, consumableMock, data.position );
  310. expect( result2 ).instanceof( ModelRange );
  311. expect( result2.start.path ).to.deep.equal( [ 1 ] );
  312. expect( result2.end.path ).to.deep.equal( [ 7 ] );
  313. expect( first( result2.getItems() ) ).to.instanceof( ModelTextProxy );
  314. expect( first( result2.getItems() ).data ).to.equal( 'foobar' );
  315. } );
  316. dispatcher.convert( new ViewDocumentFragment() );
  317. expect( spy.calledOnce ).to.be.true;
  318. expect( spyP.calledOnce ).to.be.true;
  319. expect( spyText.calledOnce ).to.be.true;
  320. } );
  321. it( 'should do nothing if element was not converted', () => {
  322. sinon.stub( log, 'warn' );
  323. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  324. spy();
  325. expect( conversionApi.convertItem( viewDiv ) ).to.equal( null );
  326. expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
  327. } );
  328. dispatcher.convert( new ViewDocumentFragment() );
  329. expect( spy.calledOnce ).to.be.true;
  330. expect( spyNull.calledOnce ).to.be.true;
  331. expect( log.warn.called ).to.be.false;
  332. log.warn.restore();
  333. } );
  334. it( 'should return null if element was incorrectly converted and log a warning', () => {
  335. sinon.stub( log, 'warn' );
  336. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  337. spy();
  338. expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
  339. } );
  340. dispatcher.convert( new ViewDocumentFragment() );
  341. expect( spy.calledOnce ).to.be.true;
  342. expect( spyArray.calledOnce ).to.be.true;
  343. expect( log.warn.calledOnce ).to.be.true;
  344. log.warn.restore();
  345. } );
  346. } );
  347. describe( 'convertChildren()', () => {
  348. it( 'should fire conversion for all children of passed element and return conversion results ' +
  349. 'wrapped in document fragment', () => {
  350. silenceWarnings();
  351. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  352. spy();
  353. const result = conversionApi.convertChildren( data.input, consumableMock, ModelPosition.createAt( rootMock ) );
  354. expect( result ).to.be.instanceof( ModelRange );
  355. expect( result.start.path ).to.deep.equal( [ 0 ] );
  356. expect( result.end.path ).to.deep.equal( [ 7 ] );
  357. expect( Array.from( result.getItems() ) ).to.length( 2 );
  358. expect( Array.from( result.getItems() )[ 0 ] ).to.equal( modelP );
  359. expect( Array.from( result.getItems() )[ 1 ] ).to.instanceof( ModelTextProxy );
  360. expect( Array.from( result.getItems() )[ 1 ].data ).to.equal( 'foobar' );
  361. } );
  362. dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ) );
  363. expect( spy.calledOnce ).to.be.true;
  364. expect( spyP.calledOnce ).to.be.true;
  365. expect( spyText.calledOnce ).to.be.true;
  366. } );
  367. it( 'should filter out incorrectly converted elements and log warnings', () => {
  368. sinon.stub( log, 'warn' );
  369. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  370. spy();
  371. const result = conversionApi.convertChildren( data.input, consumableMock, ModelPosition.createAt( rootMock ) );
  372. expect( result ).to.be.instanceof( ModelRange );
  373. expect( result.start.path ).to.deep.equal( [ 0 ] );
  374. expect( result.end.path ).to.deep.equal( [ 7 ] );
  375. expect( Array.from( result.getItems() ) ).to.length( 2 );
  376. expect( Array.from( result.getItems() )[ 0 ] ).to.equal( modelP );
  377. expect( Array.from( result.getItems() )[ 1 ] ).to.instanceof( ModelTextProxy );
  378. expect( Array.from( result.getItems() )[ 1 ].data ).to.equal( 'foobar' );
  379. } );
  380. dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ) );
  381. expect( spy.calledOnce ).to.be.true;
  382. expect( spyNull.calledOnce ).to.be.true;
  383. expect( spyArray.calledOnce ).to.be.true;
  384. expect( log.warn.calledOnce ).to.be.true;
  385. log.warn.restore();
  386. } );
  387. } );
  388. describe( 'splitToAllowedParent()', () => {
  389. beforeEach( () => {
  390. model.schema.register( 'paragraph', {
  391. allowIn: '$root'
  392. } );
  393. } );
  394. it( 'should return current position if element is allowed on this position', () => {
  395. const spy = sinon.spy();
  396. model.schema.register( 'span', {
  397. allowIn: 'paragraph'
  398. } );
  399. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  400. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  401. const span = conversionApi.writer.createElement( 'span' );
  402. const position = ModelPosition.createAt( paragraph );
  403. const result = conversionApi.splitToAllowedParent( span, position );
  404. expect( result ).to.deep.equal( { position } );
  405. spy();
  406. } );
  407. dispatcher.convert( new ViewDocumentFragment() );
  408. sinon.assert.calledOnce( spy );
  409. } );
  410. it( 'should split position to allowed ancestor if element is allowed in one of ancestors', () => {
  411. const spy = sinon.spy();
  412. model.schema.register( 'section', {
  413. allowIn: '$root'
  414. } );
  415. model.schema.register( 'span', {
  416. allowIn: 'paragraph'
  417. } );
  418. model.schema.extend( 'paragraph', {
  419. allowIn: 'section'
  420. } );
  421. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  422. const section = conversionApi.writer.createElement( 'section' );
  423. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  424. const span = conversionApi.writer.createElement( 'span' );
  425. conversionApi.writer.insert( paragraph, section );
  426. conversionApi.writer.insert( span, paragraph );
  427. const position = ModelPosition.createAt( span );
  428. const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
  429. const result = conversionApi.splitToAllowedParent( paragraph2, position );
  430. expect( result ).to.deep.equal( {
  431. position: ModelPosition.createAfter( paragraph ),
  432. endElement: paragraph.parent.getChild( 1 ).getChild( 0 )
  433. } );
  434. spy();
  435. } );
  436. dispatcher.convert( new ViewDocumentFragment() );
  437. sinon.assert.calledOnce( spy );
  438. } );
  439. it( 'should return null if element is not allowed in position and any of ancestors', () => {
  440. const spy = sinon.spy();
  441. model.schema.register( 'span' );
  442. dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
  443. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  444. const span = conversionApi.writer.createElement( 'span' );
  445. const position = ModelPosition.createAt( paragraph );
  446. const result = conversionApi.splitToAllowedParent( span, position );
  447. expect( result ).to.null;
  448. spy();
  449. } );
  450. dispatcher.convert( new ViewDocumentFragment() );
  451. sinon.assert.calledOnce( spy );
  452. } );
  453. } );
  454. } );
  455. // Silences warnings that pop up in tests. Use when the test checks a specific functionality and we are not interested in those logs.
  456. // No need to restore `log.warn` - it is done in `afterEach()`.
  457. function silenceWarnings() {
  458. log.warn = () => {};
  459. }
  460. } );