view.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document, console */
  6. import View from '../../../src/view/view';
  7. import Observer from '../../../src/view/observer/observer';
  8. import MutationObserver from '../../../src/view/observer/mutationobserver';
  9. import KeyObserver from '../../../src/view/observer/keyobserver';
  10. import InputObserver from '../../../src/view/observer/inputobserver';
  11. import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
  12. import SelectionObserver from '../../../src/view/observer/selectionobserver';
  13. import FocusObserver from '../../../src/view/observer/focusobserver';
  14. import CompositionObserver from '../../../src/view/observer/compositionobserver';
  15. import ViewRange from '../../../src/view/range';
  16. import ViewElement from '../../../src/view/element';
  17. import ViewPosition from '../../../src/view/position';
  18. import ViewSelection from '../../../src/view/selection';
  19. import count from '@ckeditor/ckeditor5-utils/src/count';
  20. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  21. import createViewRoot from '../_utils/createroot';
  22. import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
  23. import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  24. import env from '@ckeditor/ckeditor5-utils/src/env';
  25. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  26. describe( 'view', () => {
  27. const DEFAULT_OBSERVERS_COUNT = 6;
  28. let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
  29. beforeEach( () => {
  30. domRoot = createElement( document, 'div', {
  31. id: 'editor',
  32. contenteditable: 'true'
  33. } );
  34. document.body.appendChild( domRoot );
  35. view = new View();
  36. viewDocument = view.document;
  37. ObserverMock = class extends Observer {
  38. constructor( view ) {
  39. super( view );
  40. this.enable = sinon.spy();
  41. this.disable = sinon.spy();
  42. this.observe = sinon.spy();
  43. this.destroy = sinon.spy();
  44. }
  45. };
  46. instantiated = 0;
  47. enabled = 0;
  48. ObserverMockGlobalCount = class extends Observer {
  49. constructor( view ) {
  50. super( view );
  51. instantiated++;
  52. this.observe = sinon.spy();
  53. }
  54. enable() {
  55. enabled++;
  56. }
  57. };
  58. } );
  59. afterEach( () => {
  60. sinon.restore();
  61. domRoot.remove();
  62. view.destroy();
  63. } );
  64. it( 'should add default observers', () => {
  65. expect( count( view._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
  66. expect( view.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
  67. expect( view.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
  68. expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
  69. expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
  70. expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
  71. expect( view.getObserver( CompositionObserver ) ).to.be.instanceof( CompositionObserver );
  72. } );
  73. it( 'should add InputObserver on Android devices', () => {
  74. const oldEnvIsAndroid = env.isAndroid;
  75. env.isAndroid = true;
  76. const newView = new View();
  77. expect( newView.getObserver( InputObserver ) ).to.be.instanceof( InputObserver );
  78. env.isAndroid = oldEnvIsAndroid;
  79. newView.destroy();
  80. } );
  81. describe( 'attachDomRoot()', () => {
  82. it( 'should attach DOM element to main view element', () => {
  83. const domDiv = document.createElement( 'div' );
  84. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  85. expect( count( view.domRoots ) ).to.equal( 0 );
  86. view.attachDomRoot( domDiv );
  87. expect( count( view.domRoots ) ).to.equal( 1 );
  88. expect( view.getDomRoot() ).to.equal( domDiv );
  89. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
  90. expect( view._renderer.markedChildren.has( viewRoot ) ).to.be.true;
  91. domDiv.remove();
  92. } );
  93. it( 'should attach DOM element to custom view element', () => {
  94. const domH1 = document.createElement( 'h1' );
  95. const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
  96. expect( count( view.domRoots ) ).to.equal( 0 );
  97. view.attachDomRoot( domH1, 'header' );
  98. expect( count( view.domRoots ) ).to.equal( 1 );
  99. expect( view.getDomRoot( 'header' ) ).to.equal( domH1 );
  100. expect( view.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
  101. expect( view._renderer.markedChildren.has( viewH1 ) ).to.be.true;
  102. } );
  103. it( 'should handle the "contenteditable" attribute management on #isReadOnly change', () => {
  104. const domDiv = document.createElement( 'div' );
  105. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  106. view.attachDomRoot( domDiv );
  107. viewRoot.isReadOnly = false;
  108. expect( viewRoot.getAttribute( 'contenteditable' ) ).to.equal( 'true' );
  109. viewRoot.isReadOnly = true;
  110. expect( viewRoot.getAttribute( 'contenteditable' ) ).to.equal( 'false' );
  111. } );
  112. it( 'should handle the ".ck-read-only" class management on #isReadOnly change', () => {
  113. const domDiv = document.createElement( 'div' );
  114. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  115. view.attachDomRoot( domDiv );
  116. viewRoot.isReadOnly = false;
  117. expect( viewRoot.hasClass( 'ck-read-only' ) ).to.be.false;
  118. viewRoot.isReadOnly = true;
  119. expect( viewRoot.hasClass( 'ck-read-only' ) ).to.be.true;
  120. } );
  121. it( 'should call observe on each observer', () => {
  122. // The variable will be overwritten.
  123. view.destroy();
  124. view = new View();
  125. viewDocument = view.document;
  126. view._renderer.render = sinon.spy();
  127. const domDiv1 = document.createElement( 'div' );
  128. domDiv1.setAttribute( 'id', 'editor' );
  129. const domDiv2 = document.createElement( 'div' );
  130. domDiv2.setAttribute( 'id', 'editor' );
  131. const observerMock = view.addObserver( ObserverMock );
  132. const observerMockGlobalCount = view.addObserver( ObserverMockGlobalCount );
  133. createViewRoot( viewDocument, 'div', 'root1' );
  134. view.attachDomRoot( document.createElement( 'div' ), 'root1' );
  135. sinon.assert.calledOnce( observerMock.observe );
  136. sinon.assert.calledOnce( observerMockGlobalCount.observe );
  137. } );
  138. } );
  139. describe( 'detachDomRoot()', () => {
  140. it( 'should remove DOM root and unbind DOM element', () => {
  141. const domDiv = document.createElement( 'div' );
  142. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  143. view.attachDomRoot( domDiv );
  144. expect( count( view.domRoots ) ).to.equal( 1 );
  145. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
  146. view.detachDomRoot( 'main' );
  147. expect( count( view.domRoots ) ).to.equal( 0 );
  148. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.be.undefined;
  149. domDiv.remove();
  150. } );
  151. it( 'should restore the DOM root attributes to the state before attachDomRoot()', () => {
  152. const domDiv = document.createElement( 'div' );
  153. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  154. domDiv.setAttribute( 'foo', 'bar' );
  155. domDiv.setAttribute( 'data-baz', 'qux' );
  156. domDiv.classList.add( 'foo-class' );
  157. view.attachDomRoot( domDiv );
  158. view.change( writer => {
  159. writer.addClass( 'addedClass', viewRoot );
  160. writer.setAttribute( 'added-attribute', 'foo', viewRoot );
  161. writer.setAttribute( 'foo', 'changed the value', viewRoot );
  162. } );
  163. view.detachDomRoot( 'main' );
  164. const attributes = {};
  165. for ( const attribute of Array.from( domDiv.attributes ) ) {
  166. attributes[ attribute.name ] = attribute.value;
  167. }
  168. expect( attributes ).to.deep.equal( {
  169. foo: 'bar',
  170. 'data-baz': 'qux',
  171. class: 'foo-class'
  172. } );
  173. domDiv.remove();
  174. } );
  175. it( 'should remove the "contenteditable" attribute from the DOM root', () => {
  176. const domDiv = document.createElement( 'div' );
  177. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  178. view.attachDomRoot( domDiv );
  179. view.forceRender();
  180. viewRoot.isReadOnly = false;
  181. expect( domDiv.getAttribute( 'contenteditable' ) ).to.equal( 'true' );
  182. view.detachDomRoot( 'main' );
  183. expect( domDiv.hasAttribute( 'contenteditable' ) ).to.be.false;
  184. domDiv.remove();
  185. } );
  186. it( 'should remove the ".ck-read-only" class from the DOM root', () => {
  187. const domDiv = document.createElement( 'div' );
  188. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  189. view.attachDomRoot( domDiv );
  190. view.forceRender();
  191. viewRoot.isReadOnly = true;
  192. expect( domDiv.classList.contains( 'ck-read-only' ) ).to.be.true;
  193. view.detachDomRoot( 'main' );
  194. expect( domDiv.classList.contains( 'ck-read-only' ) ).to.be.false;
  195. domDiv.remove();
  196. } );
  197. } );
  198. describe( 'addObserver()', () => {
  199. beforeEach( () => {
  200. // The variable will be overwritten.
  201. view.destroy();
  202. view = new View();
  203. viewDocument = view.document;
  204. view._renderer.render = sinon.spy();
  205. } );
  206. afterEach( () => {
  207. view.destroy();
  208. } );
  209. it( 'should be instantiated and enabled on adding', () => {
  210. const observerMock = view.addObserver( ObserverMock );
  211. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
  212. expect( observerMock ).to.have.property( 'document', viewDocument );
  213. sinon.assert.calledOnce( observerMock.enable );
  214. } );
  215. it( 'should return observer instance each time addObserver is called', () => {
  216. const observerMock1 = view.addObserver( ObserverMock );
  217. const observerMock2 = view.addObserver( ObserverMock );
  218. expect( observerMock1 ).to.be.instanceof( ObserverMock );
  219. expect( observerMock2 ).to.be.instanceof( ObserverMock );
  220. expect( observerMock1 ).to.equals( observerMock2 );
  221. } );
  222. it( 'should instantiate one observer only once', () => {
  223. view.addObserver( ObserverMockGlobalCount );
  224. view.addObserver( ObserverMockGlobalCount );
  225. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
  226. expect( instantiated ).to.equal( 1 );
  227. expect( enabled ).to.equal( 1 );
  228. view.addObserver( ObserverMock );
  229. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
  230. } );
  231. it( 'should instantiate child class of already registered observer', () => {
  232. class ObserverMock extends Observer {
  233. enable() {}
  234. }
  235. class ChildObserverMock extends ObserverMock {
  236. enable() {}
  237. }
  238. view.addObserver( ObserverMock );
  239. view.addObserver( ChildObserverMock );
  240. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
  241. } );
  242. it( 'should be disabled and re-enabled on render', () => {
  243. const observerMock = view.addObserver( ObserverMock );
  244. view.forceRender();
  245. sinon.assert.calledOnce( observerMock.disable );
  246. sinon.assert.calledOnce( view._renderer.render );
  247. sinon.assert.calledTwice( observerMock.enable );
  248. } );
  249. it( 'should call observe on each root', () => {
  250. createViewRoot( viewDocument, 'div', 'roo1' );
  251. createViewRoot( viewDocument, 'div', 'roo2' );
  252. view.attachDomRoot( document.createElement( 'div' ), 'roo1' );
  253. view.attachDomRoot( document.createElement( 'div' ), 'roo2' );
  254. const observerMock = view.addObserver( ObserverMock );
  255. sinon.assert.calledTwice( observerMock.observe );
  256. } );
  257. } );
  258. describe( 'getObserver()', () => {
  259. it( 'should return observer it it is added', () => {
  260. const addedObserverMock = view.addObserver( ObserverMock );
  261. const getObserverMock = view.getObserver( ObserverMock );
  262. expect( getObserverMock ).to.be.instanceof( ObserverMock );
  263. expect( getObserverMock ).to.equal( addedObserverMock );
  264. } );
  265. it( 'should return undefined if observer is not added', () => {
  266. const getObserverMock = view.getObserver( ObserverMock );
  267. expect( getObserverMock ).to.be.undefined;
  268. } );
  269. } );
  270. describe( 'scrollToTheSelection()', () => {
  271. beforeEach( () => {
  272. // Silence the Rect warnings.
  273. sinon.stub( console, 'warn' );
  274. } );
  275. it( 'does nothing when there are no ranges in the selection', () => {
  276. const stub = sinon.stub( global.window, 'scrollTo' );
  277. view.scrollToTheSelection();
  278. sinon.assert.notCalled( stub );
  279. } );
  280. it( 'scrolls to the first range in selection with an offset', () => {
  281. const root = createViewRoot( viewDocument, 'div', 'main' );
  282. const stub = sinon.stub( global.window, 'scrollTo' );
  283. const range = ViewRange._createIn( root );
  284. view.attachDomRoot( domRoot );
  285. view.change( writer => {
  286. writer.setSelection( range );
  287. } );
  288. // Make sure the window will have to scroll to the domRoot.
  289. Object.assign( domRoot.style, {
  290. position: 'absolute',
  291. top: '-1000px',
  292. left: '-1000px'
  293. } );
  294. view.scrollToTheSelection();
  295. sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
  296. } );
  297. } );
  298. describe( 'disableObservers()', () => {
  299. it( 'should disable observers', () => {
  300. const addedObserverMock = view.addObserver( ObserverMock );
  301. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  302. expect( addedObserverMock.disable.called ).to.be.false;
  303. view.disableObservers();
  304. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  305. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  306. } );
  307. } );
  308. describe( 'enableObservers()', () => {
  309. it( 'should enable observers', () => {
  310. const addedObserverMock = view.addObserver( ObserverMock );
  311. view.disableObservers();
  312. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  313. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  314. view.enableObservers();
  315. expect( addedObserverMock.enable.calledTwice ).to.be.true;
  316. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  317. } );
  318. } );
  319. describe( 'focus()', () => {
  320. let domEditable, viewEditable;
  321. beforeEach( () => {
  322. domEditable = document.createElement( 'div' );
  323. domEditable.setAttribute( 'contenteditable', 'true' );
  324. document.body.appendChild( domEditable );
  325. viewEditable = createViewRoot( viewDocument, 'div', 'main' );
  326. view.attachDomRoot( domEditable );
  327. view.change( writer => {
  328. writer.setSelection( viewEditable, 0 );
  329. } );
  330. } );
  331. afterEach( () => {
  332. document.body.removeChild( domEditable );
  333. } );
  334. it( 'should focus editable with selection', () => {
  335. const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
  336. const renderSpy = sinon.spy( view, 'forceRender' );
  337. view.focus();
  338. expect( converterFocusSpy.called ).to.be.true;
  339. expect( renderSpy.calledOnce ).to.be.true;
  340. expect( document.activeElement ).to.equal( domEditable );
  341. const domSelection = document.getSelection();
  342. expect( domSelection.rangeCount ).to.equal( 1 );
  343. const domRange = domSelection.getRangeAt( 0 );
  344. expect( domRange.startContainer ).to.equal( domEditable );
  345. expect( domRange.startOffset ).to.equal( 0 );
  346. expect( domRange.collapsed ).to.be.true;
  347. } );
  348. it( 'should not focus if document is already focused', () => {
  349. const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
  350. const renderSpy = sinon.spy( view, 'forceRender' );
  351. viewDocument.isFocused = true;
  352. view.focus();
  353. expect( converterFocusSpy.called ).to.be.false;
  354. expect( renderSpy.called ).to.be.false;
  355. } );
  356. it( 'should not crash when there is no selection', () => {
  357. // Catches the `There is no selection in any editable to focus.` warning in the CK_DEBUG mode.
  358. sinon.stub( console, 'warn' );
  359. view.change( writer => {
  360. writer.setSelection( null );
  361. } );
  362. view.focus();
  363. } );
  364. } );
  365. describe( 'isFocused', () => {
  366. it( 'should change renderer.isFocused too', () => {
  367. expect( viewDocument.isFocused ).to.equal( false );
  368. expect( view._renderer.isFocused ).to.equal( false );
  369. viewDocument.isFocused = true;
  370. expect( viewDocument.isFocused ).to.equal( true );
  371. expect( view._renderer.isFocused ).to.equal( true );
  372. } );
  373. } );
  374. describe( 'isRenderingInProgress', () => {
  375. it( 'should be true while rendering is in progress', () => {
  376. expect( view.isRenderingInProgress ).to.equal( false );
  377. const spy = sinon.spy();
  378. view.on( 'change:isRenderingInProgress', spy );
  379. view.fire( 'render' );
  380. sinon.assert.calledTwice( spy );
  381. sinon.assert.calledWith( spy.firstCall, sinon.match.any, 'isRenderingInProgress', true );
  382. sinon.assert.calledWith( spy.secondCall, sinon.match.any, 'isRenderingInProgress', false );
  383. } );
  384. } );
  385. describe( 'forceRender()', () => {
  386. it( 'disable observers, renders and enable observers', () => {
  387. const observerMock = view.addObserver( ObserverMock );
  388. const renderStub = sinon.stub( view._renderer, 'render' );
  389. view.forceRender();
  390. sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
  391. } );
  392. it( 'should fire `render` and `layoutChanged` even if there were no changes', () => {
  393. const renderSpy = sinon.spy();
  394. const layoutChangedSpy = sinon.spy();
  395. view.on( 'render', renderSpy );
  396. view.document.on( 'layoutChanged', layoutChangedSpy );
  397. view.forceRender();
  398. sinon.assert.calledOnce( renderSpy );
  399. sinon.assert.calledOnce( layoutChangedSpy );
  400. } );
  401. it( 'should fire `render` and `layoutChanged` if there is some buffered change', () => {
  402. const renderSpy = sinon.spy();
  403. const layoutChangedSpy = sinon.spy();
  404. view.on( 'render', renderSpy );
  405. view.document.on( 'layoutChanged', layoutChangedSpy );
  406. view.document.selection._setTo( null );
  407. view.forceRender();
  408. sinon.assert.calledOnce( renderSpy );
  409. sinon.assert.calledOnce( layoutChangedSpy );
  410. } );
  411. } );
  412. describe( 'view and DOM integration', () => {
  413. it( 'should remove content of the DOM', () => {
  414. const domDiv = createElement( document, 'div', { id: 'editor' }, [
  415. createElement( document, 'p' ),
  416. createElement( document, 'p' )
  417. ] );
  418. const view = new View();
  419. const viewDocument = view.document;
  420. createViewRoot( viewDocument, 'div', 'main' );
  421. view.attachDomRoot( domDiv );
  422. view.forceRender();
  423. expect( domDiv.childNodes.length ).to.equal( 1 );
  424. expect( view.domConverter.isBlockFiller( domDiv.childNodes[ 0 ] ) ).to.be.true;
  425. view.destroy();
  426. domDiv.remove();
  427. } );
  428. it( 'should render changes in the Document', () => {
  429. const domDiv = document.createElement( 'div' );
  430. const view = new View();
  431. const viewDocument = view.document;
  432. createViewRoot( viewDocument, 'div', 'main' );
  433. view.attachDomRoot( domDiv );
  434. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  435. view.forceRender();
  436. expect( domDiv.childNodes.length ).to.equal( 1 );
  437. expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
  438. view.destroy();
  439. } );
  440. it( 'should render attribute changes', () => {
  441. const domRoot = document.createElement( 'div' );
  442. const view = new View();
  443. const viewDocument = view.document;
  444. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  445. view.attachDomRoot( domRoot );
  446. const viewP = new ViewElement( 'p', { class: 'foo' } );
  447. viewRoot._appendChild( viewP );
  448. view.forceRender();
  449. expect( domRoot.childNodes.length ).to.equal( 1 );
  450. expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
  451. viewP._setAttribute( 'class', 'bar' );
  452. view.forceRender();
  453. expect( domRoot.childNodes.length ).to.equal( 1 );
  454. expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
  455. view.destroy();
  456. domRoot.remove();
  457. } );
  458. } );
  459. describe( 'change()', () => {
  460. it( 'should throw when someone tries to change view during rendering', () => {
  461. const domDiv = document.createElement( 'div' );
  462. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  463. let renderingCalled = false;
  464. view.attachDomRoot( domDiv );
  465. view.change( writer => {
  466. const p = writer.createContainerElement( 'p' );
  467. const ui = writer.createUIElement( 'span', null, function( domDocument ) {
  468. const element = this.toDomElement( domDocument );
  469. expectToThrowCKEditorError( () => {
  470. view.change( () => {} );
  471. }, /^cannot-change-view-tree/, view );
  472. renderingCalled = true;
  473. return element;
  474. } );
  475. writer.insert( ViewPosition._createAt( p, 0 ), ui );
  476. writer.insert( ViewPosition._createAt( viewRoot, 0 ), p );
  477. } );
  478. expect( renderingCalled ).to.be.true;
  479. domDiv.remove();
  480. } );
  481. it( 'should throw when someone tries to use change() method in post-fixer', () => {
  482. const domDiv = document.createElement( 'div' );
  483. createViewRoot( viewDocument, 'div', 'main' );
  484. view.attachDomRoot( domDiv );
  485. viewDocument.registerPostFixer( () => {
  486. expectToThrowCKEditorError( () => {
  487. view.change( () => {} );
  488. }, /^cannot-change-view-tree/, view );
  489. } );
  490. view.forceRender();
  491. domDiv.remove();
  492. } );
  493. it( 'should fire render event and it should trigger rendering before listeners on normal priority', () => {
  494. const renderSpy = sinon.spy( view._renderer, 'render' );
  495. const eventSpy = sinon.spy();
  496. view.on( 'render', eventSpy );
  497. view.change( writer => {
  498. writer.setSelection( null );
  499. } );
  500. sinon.assert.callOrder( renderSpy, eventSpy );
  501. } );
  502. it( 'should fire render event once for nested change blocks', () => {
  503. const renderSpy = sinon.spy( view._renderer, 'render' );
  504. const eventSpy = sinon.spy();
  505. const viewEditable = createViewRoot( viewDocument, 'div', 'main' );
  506. view.on( 'render', eventSpy );
  507. view.change( writer => {
  508. writer.setSelection( null );
  509. view.change( writer => {
  510. writer.setSelection( viewEditable, 0 );
  511. } );
  512. view.change( writer => {
  513. writer.setSelection( null );
  514. view.change( writer => {
  515. writer.setSelection( viewEditable, 0 );
  516. } );
  517. } );
  518. view.change( writer => {
  519. writer.setSelection( null );
  520. } );
  521. } );
  522. sinon.assert.calledOnce( renderSpy );
  523. sinon.assert.calledOnce( eventSpy );
  524. sinon.assert.callOrder( renderSpy, eventSpy );
  525. } );
  526. it( 'should fire render event once even if render is called during the change', () => {
  527. const renderSpy = sinon.spy( view._renderer, 'render' );
  528. const eventSpy = sinon.spy();
  529. view.on( 'render', eventSpy );
  530. view.change( () => {
  531. view.forceRender();
  532. view.change( writer => {
  533. writer.setSelection( null );
  534. view.forceRender();
  535. } );
  536. view.forceRender();
  537. } );
  538. sinon.assert.calledOnce( renderSpy );
  539. sinon.assert.calledOnce( eventSpy );
  540. sinon.assert.callOrder( renderSpy, eventSpy );
  541. } );
  542. it( 'should call post-fixers after change but before rendering', () => {
  543. const postFixer1 = sinon.spy( () => false );
  544. const postFixer2 = sinon.spy( () => false );
  545. const changeSpy = sinon.spy();
  546. const eventSpy = sinon.spy();
  547. viewDocument.registerPostFixer( postFixer1 );
  548. viewDocument.registerPostFixer( postFixer2 );
  549. view.on( 'render', eventSpy );
  550. view.change( writer => {
  551. changeSpy();
  552. writer.setSelection( null );
  553. } );
  554. sinon.assert.calledOnce( postFixer1 );
  555. sinon.assert.calledOnce( postFixer2 );
  556. sinon.assert.calledOnce( changeSpy );
  557. sinon.assert.calledOnce( eventSpy );
  558. sinon.assert.callOrder( changeSpy, postFixer1, postFixer2, eventSpy );
  559. } );
  560. it( 'should call post-fixers until all are done', () => {
  561. let called = false;
  562. const postFixer1 = sinon.spy();
  563. const postFixer2 = sinon.spy();
  564. const changeSpy = sinon.spy();
  565. const eventSpy = sinon.spy();
  566. viewDocument.registerPostFixer( () => {
  567. if ( !called ) {
  568. called = true;
  569. postFixer1();
  570. return true;
  571. }
  572. postFixer2();
  573. return false;
  574. } );
  575. view.on( 'render', eventSpy );
  576. view.change( writer => {
  577. changeSpy();
  578. writer.setSelection( null );
  579. } );
  580. sinon.assert.calledOnce( postFixer1 );
  581. sinon.assert.calledOnce( postFixer2 );
  582. sinon.assert.calledOnce( changeSpy );
  583. sinon.assert.calledOnce( eventSpy );
  584. sinon.assert.callOrder( changeSpy, postFixer1, postFixer2, eventSpy );
  585. } );
  586. it( 'should return result of the callback', () => {
  587. const result = view.change( () => {
  588. return 'FooBar';
  589. } );
  590. expect( result ).to.equal( 'FooBar' );
  591. } );
  592. it( 'should return result of the callback with nested change block', () => {
  593. let result2 = false;
  594. let result3 = false;
  595. const result1 = view.change( () => {
  596. return view.change( () => {
  597. result2 = view.change( () => {
  598. return true;
  599. } );
  600. result3 = view.change( () => {} );
  601. return 42;
  602. } );
  603. } );
  604. expect( result1 ).to.equal( 42 );
  605. expect( result2 ).to.equal( true );
  606. expect( result3 ).to.undefined;
  607. } );
  608. it( 'should catch native errors and wrap them into the CKEditorError errors', () => {
  609. const error = new TypeError( 'foo' );
  610. error.stack = 'bar';
  611. expectToThrowCKEditorError( () => {
  612. view.change( () => {
  613. throw error;
  614. } );
  615. }, /view-change-unexpected-error/, view, {
  616. originalError: {
  617. message: 'foo',
  618. stack: 'bar',
  619. name: 'TypeError'
  620. }
  621. } );
  622. } );
  623. it( 'should re-throw custom CKEditorError errors', () => {
  624. const error = new TypeError( 'foo' );
  625. error.stack = 'bar';
  626. expectToThrowCKEditorError( () => {
  627. view.change( () => {
  628. throw new CKEditorError( 'foo', view );
  629. } );
  630. }, /foo/, view );
  631. } );
  632. } );
  633. describe( 'createPositionAt()', () => {
  634. it( 'should return instance of Position', () => {
  635. createViewRoot( viewDocument, 'div', 'main' );
  636. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  637. expect( view.createPositionAt( viewDocument.getRoot(), 0 ) ).to.be.instanceof( ViewPosition );
  638. } );
  639. } );
  640. describe( 'createPositionAfter()', () => {
  641. it( 'should return instance of Position', () => {
  642. createViewRoot( viewDocument, 'div', 'main' );
  643. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  644. expect( view.createPositionAfter( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewPosition );
  645. } );
  646. } );
  647. describe( 'createPositionBefore()', () => {
  648. it( 'should return instance of Position', () => {
  649. createViewRoot( viewDocument, 'div', 'main' );
  650. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  651. expect( view.createPositionBefore( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewPosition );
  652. } );
  653. } );
  654. describe( 'createRange()', () => {
  655. it( 'should return instance of Range', () => {
  656. createViewRoot( viewDocument, 'div', 'main' );
  657. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  658. expect( view.createRange( view.createPositionAt( viewDocument.getRoot(), 0 ) ) ).to.be.instanceof( ViewRange );
  659. } );
  660. } );
  661. describe( 'createRangeIn()', () => {
  662. it( 'should return instance of Range', () => {
  663. createViewRoot( viewDocument, 'div', 'main' );
  664. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  665. expect( view.createRangeIn( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewRange );
  666. } );
  667. } );
  668. describe( 'createRangeOn()', () => {
  669. it( 'should return instance of Range', () => {
  670. createViewRoot( viewDocument, 'div', 'main' );
  671. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  672. expect( view.createRangeOn( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewRange );
  673. } );
  674. } );
  675. describe( 'createSelection()', () => {
  676. it( 'should return instance of Selection', () => {
  677. createViewRoot( viewDocument, 'div', 'main' );
  678. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  679. expect( view.createSelection() ).to.be.instanceof( ViewSelection );
  680. } );
  681. } );
  682. describe( 'destroy()', () => {
  683. it( 'should destroy all observers', () => {
  684. const observerMock = view.addObserver( ObserverMock );
  685. view.destroy();
  686. sinon.assert.calledOnce( observerMock.destroy );
  687. } );
  688. } );
  689. } );