100.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  6. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  7. import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
  8. import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
  9. import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
  10. import Input from '../../src/input';
  11. import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
  12. import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
  13. import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
  14. import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver';
  15. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  16. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  17. /* global document */
  18. // NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what
  19. // should be in "newChildren" because we don't have yet access to these nodes. We pass new instances,
  20. // but this means that DomConverter which is used somewhere internally may return a different instance
  21. // (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the
  22. // browser will keep the instances of the old elements when modifying the tree when the user is typing
  23. // or if it will create new instances itself too.
  24. // However, the code handling these mutations doesn't really care what's inside new/old children. It
  25. // just needs the mutations common ancestor to understand how big fragment of the tree has changed.
  26. describe( 'Bug ckeditor5-typing#100', () => {
  27. let domElement, domRoot, editor, model, view, viewDocument, viewRoot;
  28. beforeEach( () => {
  29. domElement = document.createElement( 'div' );
  30. document.body.appendChild( domElement );
  31. return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEditing ] } )
  32. .then( newEditor => {
  33. editor = newEditor;
  34. model = editor.model;
  35. view = editor.editing.view;
  36. viewDocument = view.document;
  37. viewRoot = viewDocument.getRoot();
  38. domRoot = view.getDomRoot();
  39. // Mock image feature.
  40. newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
  41. editor.conversion.for( 'downcast' ).elementToElement( {
  42. model: 'image',
  43. view: 'img'
  44. } );
  45. editor.conversion.for( 'upcast' ).elementToElement( {
  46. view: 'img',
  47. model: 'image'
  48. } );
  49. // Disable MO completely and in a way it won't be reenabled on some Document#render() call.
  50. const mutationObserver = view.getObserver( MutationObserver );
  51. mutationObserver.disable();
  52. mutationObserver.enable = () => {};
  53. } );
  54. } );
  55. afterEach( () => {
  56. domElement.remove();
  57. return editor.destroy();
  58. } );
  59. // This happens when browser automatically switches parent and child nodes.
  60. it( 'should handle mutations switching inner and outer node when adding new text node after', () => {
  61. setModelData( model,
  62. '<paragraph>' +
  63. '<$text italic="true" bold="true">' +
  64. 'text[]' +
  65. '</$text>' +
  66. '</paragraph>'
  67. );
  68. expect( getViewData( view ) ).to.equal( '<p><i><strong>text{}</strong></i></p>' );
  69. const paragraph = viewRoot.getChild( 0 );
  70. const italic = paragraph.getChild( 0 );
  71. const strong = italic.getChild( 0 );
  72. const text = strong.getChild( 0 );
  73. // Simulate mutations and DOM change.
  74. domRoot.childNodes[ 0 ].innerHTML = '<strong><i>text</i>x</strong>';
  75. viewDocument.fire( 'mutations', [
  76. // First mutation - remove all children from link element.
  77. {
  78. type: 'children',
  79. node: italic,
  80. oldChildren: [ strong ],
  81. newChildren: []
  82. },
  83. // Second mutation - remove link from paragraph and put italic there.
  84. {
  85. type: 'children',
  86. node: paragraph,
  87. oldChildren: [ italic ],
  88. newChildren: [ new ViewElement( viewDocument, 'i' ) ]
  89. },
  90. // Third mutation - italic's new children.
  91. {
  92. type: 'children',
  93. node: strong,
  94. oldChildren: [ text ],
  95. newChildren: [ new ViewElement( viewDocument, 'a', null, text._clone() ), new ViewText( viewDocument, 'x' ) ]
  96. }
  97. ] );
  98. expect( getViewData( view ) ).to.equal( '<p><i><strong>textx{}</strong></i></p>' );
  99. } );
  100. it( 'should handle mutations switching inner and outer node when adding new text node before', () => {
  101. setModelData( model,
  102. '<paragraph>' +
  103. '<$text italic="true" bold="true">' +
  104. '[]text' +
  105. '</$text>' +
  106. '</paragraph>'
  107. );
  108. expect( getViewData( view ) ).to.equal( '<p><i><strong>{}text</strong></i></p>' );
  109. const paragraph = viewRoot.getChild( 0 );
  110. const italic = paragraph.getChild( 0 );
  111. const strong = italic.getChild( 0 );
  112. const text = strong.getChild( 0 );
  113. // Simulate mutations and DOM change.
  114. domRoot.childNodes[ 0 ].innerHTML = '<strong>x<i>text</i></strong>';
  115. viewDocument.fire( 'mutations', [
  116. // First mutation - remove all children from link element.
  117. {
  118. type: 'children',
  119. node: italic,
  120. oldChildren: [ strong ],
  121. newChildren: []
  122. },
  123. // Second mutation - remove link from paragraph and put italic there.
  124. {
  125. type: 'children',
  126. node: paragraph,
  127. oldChildren: [ italic ],
  128. newChildren: [ new ViewElement( viewDocument, 'i' ) ]
  129. },
  130. // Third mutation - italic's new children.
  131. {
  132. type: 'children',
  133. node: strong,
  134. oldChildren: [ text ],
  135. newChildren: [ new ViewText( viewDocument, 'x' ), new ViewElement( viewDocument, 'a', null, 'text' ) ]
  136. }
  137. ] );
  138. expect( getViewData( view ) ).to.equal( '<p><i><strong>x{}text</strong></i></p>' );
  139. } );
  140. it( 'should handle mutations switching inner and outer node - with text before', () => {
  141. setModelData( model,
  142. '<paragraph>' +
  143. 'xxx<$text italic="true" bold="true">' +
  144. 'text[]' +
  145. '</$text>' +
  146. '</paragraph>'
  147. );
  148. expect( getViewData( view ) ).to.equal( '<p>xxx<i><strong>text{}</strong></i></p>' );
  149. const paragraph = viewRoot.getChild( 0 );
  150. const textBefore = paragraph.getChild( 0 );
  151. const link = paragraph.getChild( 1 );
  152. const italic = link.getChild( 0 );
  153. const text = italic.getChild( 0 );
  154. // Simulate mutations and DOM change.
  155. domRoot.childNodes[ 0 ].innerHTML = 'xxx<strong><i>text</i>x</strong>';
  156. viewDocument.fire( 'mutations', [
  157. // First mutation - remove all children from link element.
  158. {
  159. type: 'children',
  160. node: link,
  161. oldChildren: [ italic ],
  162. newChildren: []
  163. },
  164. // Second mutation - remove link from paragraph and put italic there.
  165. {
  166. type: 'children',
  167. node: paragraph,
  168. oldChildren: [ textBefore, link ],
  169. newChildren: [ new ViewText( viewDocument, 'xxx' ), new ViewElement( viewDocument, 'i' ) ]
  170. },
  171. // Third mutation - italic's new children.
  172. {
  173. type: 'children',
  174. node: italic,
  175. oldChildren: [ text ],
  176. newChildren: [ new ViewElement( viewDocument, 'a', null, 'text' ), new ViewText( viewDocument, 'x' ) ]
  177. }
  178. ] );
  179. expect( getViewData( view ) ).to.equal( '<p>xxx<i><strong>textx{}</strong></i></p>' );
  180. } );
  181. // This happens when spell checker is applied on <strong> element and changes it to <b>.
  182. it( 'should handle mutations replacing node', () => {
  183. setModelData( model,
  184. '<paragraph>' +
  185. '<$text bold="true">' +
  186. 'text[]' +
  187. '</$text>' +
  188. '</paragraph>'
  189. );
  190. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  191. const paragraph = viewRoot.getChild( 0 );
  192. const strong = paragraph.getChild( 0 );
  193. // Simulate mutations and DOM change.
  194. domRoot.childNodes[ 0 ].innerHTML = '<b>fixed text</b>';
  195. viewDocument.fire( 'mutations', [
  196. // Replace `<strong>` with `<b>`.
  197. {
  198. type: 'children',
  199. node: paragraph,
  200. oldChildren: [ strong ],
  201. newChildren: [ new ViewElement( viewDocument, 'b', null, 'fixed text' ) ]
  202. }
  203. ] );
  204. expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>fixed text</strong></p>' );
  205. } );
  206. // Spell checker splits text inside attributes to two text nodes.
  207. it( 'should handle mutations inside attribute element', () => {
  208. setModelData( model,
  209. '<paragraph>' +
  210. '<$text bold="true">' +
  211. 'this is foo text[]' +
  212. '</$text>' +
  213. '</paragraph>'
  214. );
  215. expect( getViewData( view ) ).to.equal( '<p><strong>this is foo text{}</strong></p>' );
  216. const paragraph = viewRoot.getChild( 0 );
  217. const strong = paragraph.getChild( 0 );
  218. const text = strong.getChild( 0 );
  219. // Simulate mutations and DOM change.
  220. domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text';
  221. viewDocument.fire( 'mutations', [
  222. {
  223. type: 'children',
  224. node: strong,
  225. oldChildren: [ text ],
  226. newChildren: [ new ViewText( viewDocument, 'this is bar' ), new ViewText( viewDocument, ' text' ) ]
  227. }
  228. ] );
  229. expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>this is bar text</strong></p>' );
  230. } );
  231. it( 'should do nothing if elements mutated', () => {
  232. setModelData( model,
  233. '<paragraph>' +
  234. '<$text bold="true">' +
  235. 'text[]' +
  236. '</$text>' +
  237. '</paragraph>'
  238. );
  239. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  240. const paragraph = viewRoot.getChild( 0 );
  241. const strong = paragraph.getChild( 0 );
  242. // Simulate mutations and DOM change.
  243. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong><img />';
  244. viewDocument.fire( 'mutations', [
  245. {
  246. type: 'children',
  247. node: paragraph,
  248. oldChildren: [ strong ],
  249. newChildren: [
  250. new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'text' ) ),
  251. new ViewElement( viewDocument, 'img' )
  252. ]
  253. }
  254. ] );
  255. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  256. } );
  257. it( 'should do nothing if text is not changed', () => {
  258. setModelData( model,
  259. '<paragraph>' +
  260. '<$text bold="true">' +
  261. 'text[]' +
  262. '</$text>' +
  263. '</paragraph>'
  264. );
  265. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  266. const paragraph = viewRoot.getChild( 0 );
  267. const strong = paragraph.getChild( 0 );
  268. // Simulate mutations and DOM change.
  269. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  270. viewDocument.fire( 'mutations', [
  271. {
  272. type: 'children',
  273. node: paragraph,
  274. oldChildren: [ strong ],
  275. newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'text' ) ) ]
  276. }
  277. ] );
  278. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  279. } );
  280. it( 'should do nothing on empty mutations', () => {
  281. setModelData( model,
  282. '<paragraph>' +
  283. '<$text bold="true">' +
  284. 'text[]' +
  285. '</$text>' +
  286. '</paragraph>'
  287. );
  288. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  289. // Simulate mutations and DOM change.
  290. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  291. viewDocument.fire( 'mutations', [] );
  292. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  293. } );
  294. it( 'should do nothing if mutations does not have common ancestor', () => {
  295. setModelData( model,
  296. '<paragraph>' +
  297. '<$text bold="true">' +
  298. 'text[]' +
  299. '</$text>' +
  300. '</paragraph>'
  301. );
  302. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  303. const paragraph = viewRoot.getChild( 0 );
  304. const strong = paragraph.getChild( 0 );
  305. // Simulate mutations and DOM change.
  306. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  307. viewDocument.fire( 'mutations', [
  308. {
  309. type: 'children',
  310. node: paragraph,
  311. oldChildren: [ strong ],
  312. newChildren: [ strong ]
  313. },
  314. {
  315. type: 'children',
  316. node: new ViewContainerElement( 'div' ),
  317. oldChildren: [],
  318. newChildren: [ new ViewText( viewDocument, 'foo' ), new ViewText( viewDocument, 'bar' ) ]
  319. }
  320. ] );
  321. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  322. } );
  323. it( 'should handle view selection if one is returned from mutations', () => {
  324. setModelData( model,
  325. '<paragraph>' +
  326. '<$text bold="true">' +
  327. 'text[]' +
  328. '</$text>' +
  329. '</paragraph>'
  330. );
  331. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  332. const paragraph = viewRoot.getChild( 0 );
  333. const strong = paragraph.getChild( 0 );
  334. const viewSelection = view.createSelection( paragraph, 0 );
  335. // Simulate mutations and DOM change.
  336. domRoot.childNodes[ 0 ].innerHTML = '<b>textx</b>';
  337. viewDocument.fire( 'mutations', [
  338. // Replace `<strong>` with `<b>`.
  339. {
  340. type: 'children',
  341. node: paragraph,
  342. oldChildren: [ strong ],
  343. newChildren: [ new ViewElement( viewDocument, 'b', null, new ViewText( viewDocument, 'textx' ) ) ]
  344. }
  345. ], viewSelection );
  346. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]textx</$text></paragraph>' );
  347. expect( getViewData( view ) ).to.equal( '<p><strong>{}textx</strong></p>' );
  348. } );
  349. // #117.
  350. it( 'should handle mixed mutations', () => {
  351. setModelData( model, '<paragraph>[]<$text bold="true">Foo bar aple</$text></paragraph>' );
  352. const paragraph = viewRoot.getChild( 0 );
  353. const strong = paragraph.getChild( 0 );
  354. const viewSelection = view.createSelection( paragraph, 0 );
  355. // Simulate mutations and DOM change.
  356. domRoot.childNodes[ 0 ].innerHTML = '<strong>Foo bar </strong><b>apple</b>';
  357. viewDocument.fire( 'mutations', [
  358. {
  359. type: 'text',
  360. oldText: 'Foo bar aple',
  361. newText: 'Foo bar ',
  362. node: viewRoot.getChild( 0 ).getChild( 0 )
  363. },
  364. {
  365. type: 'children',
  366. oldChildren: [ strong ],
  367. newChildren: [ strong, new ViewElement( viewDocument, 'b', null, new ViewText( viewDocument, 'apple' ) ) ],
  368. node: paragraph
  369. }
  370. ], viewSelection );
  371. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]Foo bar apple</$text></paragraph>' );
  372. expect( getViewData( view ) ).to.equal( '<p><strong>{}Foo bar apple</strong></p>' );
  373. } );
  374. } );