view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document, setTimeout, HTMLElement */
  6. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  7. import View from '../src/view';
  8. import Template from '../src/template';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  11. import ViewCollection from '../src/viewcollection';
  12. let TestView, view, childA, childB;
  13. testUtils.createSinonSandbox();
  14. describe( 'View', () => {
  15. describe( 'constructor()', () => {
  16. beforeEach( () => {
  17. setTestViewClass();
  18. setTestViewInstance();
  19. } );
  20. it( 'defines basic view properties', () => {
  21. view = new View();
  22. expect( view.t ).to.be.undefined;
  23. expect( view.locale ).to.be.undefined;
  24. expect( view.ready ).to.be.false;
  25. expect( view.template ).to.be.undefined;
  26. expect( view._viewCollections ).to.be.instanceOf( Collection );
  27. expect( view._unboundChildren ).to.be.instanceOf( ViewCollection );
  28. } );
  29. it( 'defines the locale property and the "t" function', () => {
  30. const locale = { t() {} };
  31. view = new View( locale );
  32. expect( view.locale ).to.equal( locale );
  33. expect( view.t ).to.equal( locale.t );
  34. } );
  35. describe( '_viewCollections', () => {
  36. it( 'manages #locale property', () => {
  37. const locale = {
  38. t() {}
  39. };
  40. const view = new View( locale );
  41. const collection = new ViewCollection();
  42. expect( view.locale ).to.equal( locale );
  43. expect( collection.locale ).to.be.undefined;
  44. view._viewCollections.add( collection );
  45. expect( collection.locale ).to.equal( view.locale );
  46. } );
  47. } );
  48. } );
  49. describe( 'createCollection()', () => {
  50. beforeEach( () => {
  51. setTestViewClass();
  52. setTestViewInstance();
  53. } );
  54. it( 'returns an instance of view collection', () => {
  55. expect( view.createCollection() ).to.be.instanceOf( ViewCollection );
  56. } );
  57. it( 'adds a new collection to the #_viewCollections', () => {
  58. expect( view._viewCollections ).to.have.length( 1 );
  59. const collection = view.createCollection();
  60. expect( view._viewCollections ).to.have.length( 2 );
  61. expect( view._viewCollections.get( 1 ) ).to.equal( collection );
  62. } );
  63. } );
  64. describe( 'addChildren()', () => {
  65. beforeEach( () => {
  66. setTestViewClass();
  67. setTestViewInstance();
  68. } );
  69. it( 'should return a promise', () => {
  70. const spy = sinon.spy();
  71. const child = {
  72. init: () => {
  73. return new Promise( resolve => {
  74. setTimeout( () => resolve(), 100 );
  75. } )
  76. .then( () => spy() );
  77. }
  78. };
  79. return view.init()
  80. .then( () => {
  81. const returned = view.addChildren( child );
  82. expect( returned ).to.be.instanceof( Promise );
  83. return returned.then( () => {
  84. sinon.assert.calledOnce( spy );
  85. } );
  86. } );
  87. } );
  88. it( 'should add a single view to #_unboundChildren', () => {
  89. expect( view._unboundChildren ).to.have.length( 0 );
  90. const child = {};
  91. return view.addChildren( child )
  92. .then( () => {
  93. expect( view._unboundChildren ).to.have.length( 1 );
  94. expect( view._unboundChildren.get( 0 ) ).to.equal( child );
  95. } );
  96. } );
  97. it( 'should support iterables', () => {
  98. expect( view._unboundChildren ).to.have.length( 0 );
  99. return view.addChildren( [ {}, {}, {} ] )
  100. .then( () => {
  101. expect( view._unboundChildren ).to.have.length( 3 );
  102. } );
  103. } );
  104. } );
  105. describe( 'init()', () => {
  106. beforeEach( createViewWithChildren );
  107. it( 'should throw if already initialized', () => {
  108. return view.init()
  109. .then( () => {
  110. view.init();
  111. throw new Error( 'This should not be executed.' );
  112. } )
  113. .catch( err => {
  114. expect( err ).to.be.instanceof( CKEditorError );
  115. expect( err.message ).to.match( /ui-view-init-re/ );
  116. } );
  117. } );
  118. it( 'returns a promise', () => {
  119. expect( view.init() ).to.be.instanceof( Promise );
  120. } );
  121. it( 'should set view#ready', () => {
  122. expect( view.ready ).to.be.false;
  123. return view.init().then( () => {
  124. expect( view.ready ).to.be.true;
  125. } );
  126. } );
  127. it( 'calls init() on all view#_viewCollections', () => {
  128. const collectionA = view.createCollection();
  129. const collectionB = view.createCollection();
  130. const spyA = testUtils.sinon.spy( collectionA, 'init' );
  131. const spyB = testUtils.sinon.spy( collectionB, 'init' );
  132. return view.init().then( () => {
  133. sinon.assert.calledOnce( spyA );
  134. sinon.assert.calledOnce( spyB );
  135. sinon.assert.callOrder( spyA, spyB );
  136. } );
  137. } );
  138. } );
  139. describe( 'bind', () => {
  140. beforeEach( () => {
  141. setTestViewClass();
  142. setTestViewInstance();
  143. } );
  144. it( 'returns a shorthand for Template binding', () => {
  145. expect( view.bindTemplate.to ).to.be.a( 'function' );
  146. expect( view.bindTemplate.if ).to.be.a( 'function' );
  147. const binding = view.bindTemplate.to( 'a' );
  148. expect( binding.observable ).to.equal( view );
  149. expect( binding.emitter ).to.equal( view );
  150. } );
  151. } );
  152. describe( 'element', () => {
  153. beforeEach( createViewInstanceWithTemplate );
  154. it( 'invokes out of #template', () => {
  155. setTestViewInstance();
  156. expect( view.element ).to.be.an.instanceof( HTMLElement );
  157. expect( view.element.nodeName ).to.equal( 'A' );
  158. } );
  159. it( 'can be explicitly declared', () => {
  160. class CustomView extends View {
  161. constructor() {
  162. super();
  163. this.element = document.createElement( 'span' );
  164. }
  165. }
  166. view = new CustomView();
  167. expect( view.element ).to.be.an.instanceof( HTMLElement );
  168. } );
  169. it( 'is null when there is no template', () => {
  170. expect( new View().element ).to.be.null;
  171. } );
  172. it( 'registers child views found in the template', () => {
  173. const view = new View();
  174. const viewA = new View();
  175. const viewB = new View();
  176. const viewC = new View();
  177. viewA.template = new Template( { tag: 'a' } );
  178. viewB.template = new Template( { tag: 'b' } );
  179. viewC.template = new Template( { tag: 'c' } );
  180. view.template = new Template( {
  181. tag: 'div',
  182. children: [
  183. viewA,
  184. viewB,
  185. {
  186. tag: 'p',
  187. children: [
  188. viewC
  189. ]
  190. },
  191. {
  192. text: 'foo'
  193. }
  194. ]
  195. } );
  196. expect( view._unboundChildren ).to.have.length( 0 );
  197. // Render the view.
  198. view.element;
  199. expect( view._unboundChildren ).to.have.length( 3 );
  200. expect( view._unboundChildren.get( 0 ) ).to.equal( viewA );
  201. expect( view._unboundChildren.get( 1 ) ).to.equal( viewB );
  202. expect( view._unboundChildren.get( 2 ) ).to.equal( viewC );
  203. } );
  204. } );
  205. describe( 'destroy()', () => {
  206. beforeEach( createViewWithChildren );
  207. it( 'should return a promise', () => {
  208. expect( view.destroy() ).to.be.instanceof( Promise );
  209. } );
  210. it( 'should set basic properties null', () => {
  211. return view.destroy().then( () => {
  212. expect( view.element ).to.be.null;
  213. expect( view.template ).to.be.null;
  214. expect( view.locale ).to.be.null;
  215. expect( view.t ).to.be.null;
  216. expect( view._unboundChildren ).to.be.null;
  217. expect( view._viewCollections ).to.be.null;
  218. } );
  219. } );
  220. it( 'clears #_unboundChildren', () => {
  221. const cached = view._unboundChildren;
  222. return view.addChildren( [ new View(), new View() ] )
  223. .then( () => {
  224. expect( cached ).to.have.length.above( 2 );
  225. return view.destroy().then( () => {
  226. expect( cached ).to.have.length( 0 );
  227. } );
  228. } );
  229. } );
  230. it( 'clears #_viewCollections', () => {
  231. const cached = view._viewCollections;
  232. expect( cached ).to.have.length( 1 );
  233. return view.destroy().then( () => {
  234. expect( cached ).to.have.length( 0 );
  235. } );
  236. } );
  237. it( 'leaves the #element in DOM', () => {
  238. const elRef = view.element;
  239. const parentEl = document.createElement( 'div' );
  240. parentEl.appendChild( view.element );
  241. return view.destroy().then( () => {
  242. expect( elRef.parentNode ).to.equal( parentEl );
  243. } );
  244. } );
  245. it( 'calls destroy() on all view#_viewCollections', () => {
  246. const collectionA = view.createCollection();
  247. const collectionB = view.createCollection();
  248. const spyA = testUtils.sinon.spy( collectionA, 'destroy' );
  249. const spyB = testUtils.sinon.spy( collectionB, 'destroy' );
  250. return view.destroy().then( () => {
  251. sinon.assert.calledOnce( spyA );
  252. sinon.assert.calledOnce( spyB );
  253. sinon.assert.callOrder( spyA, spyB );
  254. } );
  255. } );
  256. it( 'destroy a template–less view', () => {
  257. view = new View();
  258. expect( () => {
  259. view.destroy();
  260. } ).to.not.throw();
  261. } );
  262. // https://github.com/ckeditor/ckeditor5-ui/issues/203
  263. it( 'waits for all #addChildren promises to resolve', () => {
  264. const spyA = sinon.spy();
  265. const spyB = sinon.spy();
  266. class DelayedInitView extends View {
  267. constructor( delay, spy ) {
  268. super();
  269. this.delay = delay;
  270. this.spy = spy;
  271. }
  272. init() {
  273. return new Promise( resolve => {
  274. setTimeout( () => resolve(), this.delay );
  275. } )
  276. .then( () => super.init() )
  277. .then( () => {
  278. this.spy();
  279. } );
  280. }
  281. }
  282. const viewA = new DelayedInitView( 200, spyA );
  283. const viewB = new DelayedInitView( 100, spyB );
  284. return view.init().then( () => {
  285. view.addChildren( [ viewA, viewB ] );
  286. return view.destroy().then( () => {
  287. expect( viewA.ready ).to.be.true;
  288. expect( viewB.ready ).to.be.true;
  289. sinon.assert.callOrder( spyB, spyA );
  290. } );
  291. } );
  292. } );
  293. } );
  294. } );
  295. function createViewInstanceWithTemplate() {
  296. setTestViewClass( { tag: 'a' } );
  297. setTestViewInstance();
  298. }
  299. function setTestViewClass( templateDef ) {
  300. TestView = class V extends View {
  301. constructor() {
  302. super();
  303. if ( templateDef ) {
  304. this.template = new Template( templateDef );
  305. }
  306. }
  307. };
  308. }
  309. function setTestViewInstance() {
  310. view = new TestView();
  311. if ( view.template ) {
  312. document.body.appendChild( view.element );
  313. }
  314. }
  315. function createViewWithChildren() {
  316. class ChildView extends View {
  317. constructor() {
  318. super();
  319. this.template = new Template( {
  320. tag: 'span'
  321. } );
  322. }
  323. }
  324. class ChildViewA extends ChildView {
  325. init() {
  326. const promise = new Promise( resolve => {
  327. setTimeout( resolve, 50 );
  328. } );
  329. return super.init().then( promise );
  330. }
  331. destroy() {
  332. const promise = new Promise( resolve => {
  333. setTimeout( resolve, 10 );
  334. } );
  335. return super.destroy().then( promise );
  336. }
  337. }
  338. class ChildViewB extends ChildView {
  339. init() {
  340. const promise = new Promise( resolve => {
  341. setTimeout( resolve, 10 );
  342. } );
  343. return super.init().then( promise );
  344. }
  345. destroy() {
  346. const promise = new Promise( resolve => {
  347. setTimeout( resolve, 50 );
  348. } );
  349. return super.destroy().then( promise );
  350. }
  351. }
  352. childA = new ChildViewA();
  353. childB = new ChildViewB();
  354. setTestViewClass( {
  355. tag: 'p',
  356. children: [ childA, childB ]
  357. } );
  358. setTestViewInstance();
  359. }