PageRenderTime 115ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

https://github.com/4amitnarayan/backbone.marionette
JavaScript | 772 lines | 602 code | 155 blank | 15 comment | 7 complexity | 004b8f3a5eca2507e3cf6f81a61bb0ec MD5 | raw file
  1. describe('composite view', function() {
  2. 'use strict';
  3. beforeEach(function() {
  4. var suite = this;
  5. // Models
  6. this.Model = Backbone.Model.extend();
  7. this.User = Backbone.Model.extend();
  8. this.Node = Backbone.Model.extend({
  9. initialize: function() {
  10. var nodes = this.get('nodes');
  11. if (nodes) {
  12. this.nodes = new suite.NodeCollection(nodes);
  13. this.unset('nodes');
  14. }
  15. }
  16. });
  17. // Collections
  18. this.Collection = Backbone.Collection.extend({
  19. model: this.Model
  20. });
  21. this.UserCollection = Backbone.Collection.extend({
  22. model: this.User
  23. });
  24. this.NodeCollection = Backbone.Collection.extend({
  25. model: this.Node
  26. });
  27. // Views
  28. this.treeViewTemplateFn = _.template('<li>name: <%= name %></li>');
  29. this.TreeView = Backbone.Marionette.CompositeView.extend({
  30. tagName: 'ul',
  31. template: this.treeViewTemplateFn,
  32. initialize: function() {
  33. this.collection = this.model.nodes;
  34. }
  35. });
  36. });
  37. describe('when a composite view has a template without a model', function() {
  38. beforeEach(function() {
  39. this.templateFn = _.template('composite template');
  40. this.ChildView = Backbone.Marionette.ItemView.extend({
  41. tagName: 'span',
  42. render: function() {
  43. this.$el.html(this.model.get('foo'));
  44. }
  45. });
  46. this.CompositeViewNoModel = Backbone.Marionette.CompositeView.extend({
  47. childView: this.ChildView,
  48. template: this.templateFn
  49. });
  50. this.m1 = new this.Model({foo: 'bar'});
  51. this.m2 = new this.Model({foo: 'baz'});
  52. this.collection = new this.Collection([this.m1, this.m2]);
  53. this.compositeView = new this.CompositeViewNoModel({
  54. collection: this.collection
  55. });
  56. this.compositeView.render();
  57. });
  58. it('should render the template', function() {
  59. expect(this.compositeView.$el).to.contain.$text('composite');
  60. });
  61. it('should render the collections items', function() {
  62. expect(this.compositeView.$el).to.contain.$text('bar');
  63. expect(this.compositeView.$el).to.contain.$text('baz');
  64. });
  65. });
  66. describe('when rendering with a overridden attachElContent', function() {
  67. beforeEach(function() {
  68. this.attachElContentStub = this.sinon.stub();
  69. this.CompositeView = Marionette.CompositeView.extend({
  70. template: function(){},
  71. attachElContent: this.attachElContentStub
  72. });
  73. this.compositeView = new this.CompositeView();
  74. this.compositeView.render();
  75. });
  76. it('should render according to the custom attachElContent logic', function() {
  77. expect(this.attachElContentStub).to.have.been.calledOnce.and.calledWith(undefined);
  78. });
  79. });
  80. describe('when a composite view has a model and a template', function() {
  81. beforeEach(function() {
  82. this.templateFn = _.template('composite <%= foo %>');
  83. this.ChildView = Backbone.Marionette.ItemView.extend({
  84. tagName: 'span',
  85. render: function() {
  86. this.$el.html(this.model.get('foo'));
  87. }
  88. });
  89. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  90. childView: this.ChildView,
  91. template: this.templateFn,
  92. onRender: function() {}
  93. });
  94. this.m1 = new this.Model({foo: 'bar'});
  95. this.m2 = new this.Model({foo: 'baz'});
  96. this.collection = new this.Collection();
  97. this.collection.add(this.m2);
  98. this.compositeView = new this.CompositeView({
  99. model: this.m1,
  100. collection: this.collection
  101. });
  102. this.compositeView.render();
  103. });
  104. it('should render the template with the model', function() {
  105. expect(this.compositeView.$el).to.contain.$text('composite bar');
  106. });
  107. it('should render the collections items', function() {
  108. expect(this.compositeView.$el).to.contain.$text('baz');
  109. });
  110. });
  111. describe('when a composite view triggers render in initialize', function() {
  112. beforeEach(function() {
  113. var suite = this;
  114. this.collectionTemplateFn = _.template('');
  115. this.collectionItemTemplateFn = _.template('<% _.each(items, function(item){ %><span><%= item.foo %></span><% }) %>');
  116. this.emptyTemplateFn = _.template('&nbsp;');
  117. this.EmptyView = Backbone.Marionette.ItemView.extend({
  118. template: this.emptyTemplateFn,
  119. tagName: 'hr',
  120. onShow: function() {
  121. suite.onShow.push('EMPTY');
  122. }
  123. });
  124. this.ChildView = Backbone.Marionette.ItemView.extend({
  125. template: this.collectionItemTemplateFn,
  126. tagName: 'span'
  127. });
  128. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  129. childView: this.ChildView,
  130. emptyView: this.EmptyView,
  131. template: this.collectionTemplateFn,
  132. initialize: function() {
  133. this.render();
  134. },
  135. onRender: function() {}
  136. });
  137. this.m1 = new this.Model({foo: 'bar'});
  138. this.compositeView = new this.CompositeView({
  139. model: this.m1,
  140. collection: new this.Collection()
  141. });
  142. this.onShow = [];
  143. this.compositeView.trigger('show');
  144. });
  145. it('should call "onShowCallbacks.add"', function() {
  146. expect(this.onShow.length === 1).to.be.ok;
  147. });
  148. });
  149. describe('when rendering a composite view without a template', function() {
  150. beforeEach(function() {
  151. this.ChildView = Backbone.Marionette.ItemView.extend({
  152. tagName: 'span',
  153. render: function() {
  154. this.$el.html(this.model.get('foo'));
  155. }
  156. });
  157. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  158. childView: this.ChildView
  159. });
  160. this.m1 = new this.Model({foo: 'bar'});
  161. this.m2 = new this.Model({foo: 'baz'});
  162. this.collection = new this.Collection();
  163. this.collection.add(this.m2);
  164. this.compositeView = new this.CompositeView({
  165. model: this.m1,
  166. collection: this.collection
  167. });
  168. });
  169. it('should throw an exception because there was no valid template', function() {
  170. expect(this.compositeView.render).to.throw('Cannot render the template since its false, null or undefined.');
  171. });
  172. });
  173. describe('when rendering a composite view', function() {
  174. beforeEach(function() {
  175. var suite = this;
  176. this.templateFn = _.template('composite <%= foo %>');
  177. this.ChildView = Backbone.Marionette.ItemView.extend({
  178. tagName: 'span',
  179. render: function() {
  180. this.$el.html(this.model.get('foo'));
  181. }
  182. });
  183. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  184. childView: this.ChildView,
  185. template: this.templateFn,
  186. onRender: function() {}
  187. });
  188. this.order = [];
  189. this.m1 = new this.Model({foo: 'bar'});
  190. this.m2 = new this.Model({foo: 'baz'});
  191. this.collection = new this.Collection();
  192. this.collection.add(this.m2);
  193. this.compositeView = new this.CompositeView({
  194. model: this.m1,
  195. collection: this.collection
  196. });
  197. this.compositeView.on('render:template', function() {
  198. suite.order.push(suite.compositeView.renderedModelView);
  199. });
  200. this.compositeView.on('render:collection', function() {
  201. suite.order.push(suite.compositeView.collection);
  202. });
  203. this.compositeView.on('render', function() {
  204. suite.order.push(suite.compositeView);
  205. });
  206. this.sinon.spy(this.compositeView, 'trigger');
  207. this.sinon.spy(this.compositeView, 'onRender');
  208. this.compositeView.render();
  209. });
  210. it('should trigger a render event for the model view', function() {
  211. expect(this.compositeView.trigger).to.have.been.calledWith('render:template');
  212. });
  213. it('should trigger a before:render event for the collection', function() {
  214. expect(this.compositeView.trigger).to.have.been.calledWith('before:render:collection', this.compositeView);
  215. });
  216. it('should trigger a render event for the collection', function() {
  217. expect(this.compositeView.trigger).to.have.been.calledWith('render:collection', this.compositeView);
  218. });
  219. it('should trigger a render event for the composite view', function() {
  220. expect(this.compositeView.trigger).to.have.been.calledWith('render', this.compositeView);
  221. });
  222. it('should guarantee rendering of the model before rendering the collection', function() {
  223. expect(this.order[0]).to.equal(this.compositeView.renderedModelView);
  224. expect(this.order[1]).to.equal(this.compositeView.collection);
  225. expect(this.order[2]).to.equal(this.compositeView);
  226. });
  227. it('should call "onRender"', function() {
  228. expect(this.compositeView.onRender).to.have.been.called;
  229. });
  230. it('should only call "onRender" once', function() {
  231. expect(this.compositeView.onRender.callCount).to.equal(1);
  232. });
  233. });
  234. describe('when rendering a composite view twice', function() {
  235. beforeEach(function() {
  236. this.templateFn = _.template('composite <%= foo %>');
  237. this.ChildView = Backbone.Marionette.ItemView.extend({
  238. tagName: 'span',
  239. render: function() {
  240. this.$el.html(this.model.get('foo'));
  241. }
  242. });
  243. this.CompositeModelView = Backbone.Marionette.CompositeView.extend({
  244. childView: this.ChildView,
  245. template: this.templateFn
  246. });
  247. this.m1 = new this.Model({foo: 'bar'});
  248. this.m2 = new this.Model({foo: 'baz'});
  249. this.collection = new this.Collection();
  250. this.collection.add(this.m2);
  251. this.compositeView = new this.CompositeModelView({
  252. model: this.m1,
  253. collection: this.collection
  254. });
  255. this.sinon.spy(this.compositeView, 'render');
  256. this.sinon.spy(this.compositeView, 'destroyChildren');
  257. this.sinon.spy(Backbone.Marionette.Renderer, 'render');
  258. this.compositeRenderSpy = this.compositeView.render;
  259. this.compositeView.render();
  260. this.compositeView.render();
  261. });
  262. it('should re-render the template view', function() {
  263. expect(Backbone.Marionette.Renderer.render.callCount).to.equal(2);
  264. });
  265. it('should destroy all of the child collection child views', function() {
  266. expect(this.compositeView.destroyChildren).to.have.been.called;
  267. expect(this.compositeView.destroyChildren.callCount).to.equal(2);
  268. });
  269. it('should re-render the collections items', function() {
  270. expect(this.compositeRenderSpy.callCount).to.equal(2);
  271. });
  272. });
  273. describe('when rendering a composite view with an empty collection and then resetting the collection', function() {
  274. beforeEach(function() {
  275. this.templateFn = _.template('composite <%= foo %>');
  276. this.ChildView = Backbone.Marionette.ItemView.extend({
  277. tagName: 'span',
  278. render: function() {
  279. this.$el.html(this.model.get('foo'));
  280. }
  281. });
  282. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  283. childView: this.ChildView,
  284. template: this.templateFn,
  285. onRender: function() {}
  286. });
  287. this.m1 = new this.Model({foo: 'bar'});
  288. this.collection = new this.Collection();
  289. this.compositeView = new this.CompositeView({
  290. model: this.m1,
  291. collection: this.collection
  292. });
  293. this.compositeView.render();
  294. this.m2 = new this.Model({foo: 'baz'});
  295. this.collection.reset([this.m2]);
  296. });
  297. it('should render the template with the model', function() {
  298. expect(this.compositeView.$el).to.contain.$text('composite bar');
  299. });
  300. it('should render the collections items', function() {
  301. expect(this.compositeView.$el).to.contain.$text('baz');
  302. });
  303. });
  304. describe('when rendering a composite view without a collection', function() {
  305. beforeEach(function() {
  306. this.templateFn = _.template('composite <%= foo %>');
  307. this.ChildView = Backbone.Marionette.ItemView.extend({
  308. tagName: 'span',
  309. render: function() {
  310. this.$el.html(this.model.get('foo'));
  311. }
  312. });
  313. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  314. childView: this.ChildView,
  315. template: this.templateFn,
  316. onRender: function() {}
  317. });
  318. this.m1 = new this.Model({foo: 'bar'});
  319. this.compositeView = new this.CompositeView({
  320. model: this.m1
  321. });
  322. this.compositeView.render();
  323. });
  324. it('should render the template with the model', function() {
  325. expect(this.compositeView.$el).to.contain.$text('composite bar');
  326. });
  327. it('should not render the collections items', function() {
  328. expect(this.compositeView.$el).not.to.contain.$text('baz');
  329. });
  330. });
  331. describe('when rendering a composite with a collection', function() {
  332. beforeEach(function() {
  333. this.templateFn = _.template('composite <%= foo %>');
  334. this.ChildView = Backbone.Marionette.ItemView.extend({
  335. tagName: 'span',
  336. render: function() {
  337. this.$el.html(this.model.get('foo'));
  338. }
  339. });
  340. this.CompositeView = Backbone.Marionette.CompositeView.extend({
  341. childView: this.ChildView,
  342. template: this.templateFn,
  343. onRender: function() {}
  344. });
  345. this.m1 = new this.Model({foo: 'bar'});
  346. this.m2 = new this.Model({foo: 'baz'});
  347. this.collection = new this.Collection([this.m2]);
  348. this.compositeView = new this.CompositeView({
  349. model: this.m1,
  350. collection: this.collection
  351. });
  352. this.compositeView.render();
  353. this.sinon.spy(this.compositeView, '_renderRoot');
  354. });
  355. describe('and then resetting the collection', function() {
  356. beforeEach(function() {
  357. this.m3 = new this.Model({foo: 'quux'});
  358. this.m4 = new this.Model({foo: 'widget'});
  359. this.collection.reset([this.m3, this.m4]);
  360. });
  361. it('should not re-render the template with the model', function() {
  362. expect(this.compositeView._renderRoot).not.to.have.been.called;
  363. });
  364. it('should render the collections items', function() {
  365. expect(this.compositeView.$el).not.to.contain.$text('baz');
  366. expect(this.compositeView.$el).to.contain.$text('quux');
  367. expect(this.compositeView.$el).to.contain.$text('widget');
  368. });
  369. });
  370. describe('and then adding to the collection', function() {
  371. beforeEach(function() {
  372. this.m3 = new this.Model({foo: 'quux'});
  373. this.collection.add(this.m3);
  374. });
  375. it('should not re-render the template with the model', function() {
  376. expect(this.compositeView._renderRoot).not.to.have.been.called;
  377. });
  378. it('should add to the collections items', function() {
  379. expect(this.compositeView.$el).to.contain.$text('bar');
  380. expect(this.compositeView.$el).to.contain.$text('baz');
  381. expect(this.compositeView.$el).to.contain.$text('quux');
  382. });
  383. });
  384. describe('and then removing from the collection', function() {
  385. beforeEach(function() {
  386. this.model = this.collection.at(0);
  387. this.collection.remove(this.model);
  388. });
  389. it('should not re-render the template with the model', function() {
  390. expect(this.compositeView._renderRoot).not.to.have.been.called;
  391. });
  392. it('should remove from the collections items', function() {
  393. expect(this.compositeView.$el).not.to.contain.$text('baz');
  394. });
  395. });
  396. });
  397. describe('when working with a composite and recursive model', function() {
  398. beforeEach(function() {
  399. this.data = {
  400. name: 'level 1',
  401. nodes: [
  402. {
  403. name: 'level 2',
  404. nodes: [
  405. {
  406. name: 'level 3'
  407. }
  408. ]
  409. }
  410. ]
  411. };
  412. this.node = new this.Node(this.data);
  413. this.treeView = new this.TreeView({
  414. model: this.node
  415. });
  416. this.treeView.render();
  417. });
  418. it('should render the template with the model', function() {
  419. expect(this.treeView.$el).to.contain.$text('level 1');
  420. });
  421. it('should render the collections items', function() {
  422. expect(this.treeView.$el).to.contain.$text('level 2');
  423. });
  424. it('should render all the levels of the nested object', function() {
  425. expect(this.treeView.$el).to.contain.$text('level 3');
  426. });
  427. });
  428. describe('when destroying a composite view', function() {
  429. beforeEach(function() {
  430. this.templateFn = _.template('composite <%= foo %>');
  431. this.ChildView = Backbone.Marionette.ItemView.extend({
  432. tagName: 'span',
  433. render: function() {
  434. this.$el.html(this.model.get('foo'));
  435. }
  436. });
  437. this.CompositeModelView = Backbone.Marionette.CompositeView.extend({
  438. childView: this.ChildView,
  439. template: this.templateFn
  440. });
  441. this.m1 = new this.Model({foo: 'bar'});
  442. this.m2 = new this.Model({foo: 'baz'});
  443. this.collection = new this.Collection();
  444. this.collection.add(this.m2);
  445. this.compositeView = new this.CompositeModelView({
  446. model: this.m1,
  447. collection: this.collection
  448. });
  449. this.sinon.spy(this.CompositeModelView.prototype, 'destroy');
  450. this.compositeView.render();
  451. this.compositeView.destroy();
  452. });
  453. it('should delete the model view', function() {
  454. expect(this.compositeView.renderedModelView).to.be.undefined;
  455. });
  456. it('should destroy the collection of views', function() {
  457. expect(this.CompositeModelView.prototype.destroy.callCount).to.equal(1);
  458. });
  459. });
  460. describe('when rendering a composite view with no model, using a template to create a grid', function() {
  461. beforeEach(function() {
  462. this.gridTemplateFn = _.template('<thead><tr><th>Username</th><th>Full Name</th><tr></thead><tbody></tbody>');
  463. this.gridRowTemplateFn = _.template('<td><%= username %></td><td><%= fullname %></td>');
  464. // A Grid Row
  465. this.GridRow = Backbone.Marionette.ItemView.extend({
  466. tagName: 'tr',
  467. template: this.gridRowTemplateFn
  468. });
  469. // The grid view
  470. this.GridView = Backbone.Marionette.CompositeView.extend({
  471. tagName: 'table',
  472. template: this.gridTemplateFn,
  473. childView: this.GridRow,
  474. attachHtml: function(collectionView, itemView) {
  475. collectionView.$('tbody').append(itemView.el);
  476. }
  477. });
  478. this.userData = [
  479. {
  480. username: 'dbailey',
  481. fullname: 'Derick Bailey'
  482. },
  483. {
  484. username: 'jbob',
  485. fullname: 'Joe Bob'
  486. },
  487. {
  488. username: 'fbar',
  489. fullname: 'Foo Bar'
  490. }
  491. ];
  492. this.userList = new this.UserCollection(this.userData);
  493. this.gridView = new this.GridView({
  494. tagName: 'table',
  495. collection: this.userList
  496. });
  497. this.gridView.render();
  498. });
  499. it('should render the table', function() {
  500. expect(this.gridView.$('th').length).not.to.equal(0);
  501. });
  502. it('should render the users', function() {
  503. var body = this.gridView.$('tbody');
  504. expect(body).to.contain.$text('dbailey');
  505. expect(body).to.contain.$text('jbob');
  506. expect(body).to.contain.$text('fbar');
  507. });
  508. });
  509. describe('when a composite view has a ui elements hash', function() {
  510. beforeEach(function() {
  511. this.gridTemplateFn = _.template('<thead><tr><th>Username</th><th>Full Name</th><tr></thead><tbody></tbody>');
  512. this.gridRowTemplateFn = _.template('<td><%= username %></td><td><%= fullname %></td>');
  513. this.GridViewWithUIBindingsTemplateFn = _.template('<thead><tr><th><%= userHeader %></th><th><%= nameHeader %></th><tr></thead><tbody></tbody>');
  514. // A Grid Row
  515. this.GridRow = Backbone.Marionette.ItemView.extend({
  516. tagName: 'tr',
  517. template: this.gridRowTemplateFn
  518. });
  519. // The grid view
  520. this.GridView = Backbone.Marionette.CompositeView.extend({
  521. tagName: 'table',
  522. template: this.gridTemplateFn,
  523. childView: this.GridRow,
  524. attachHtml: function(collectionView, itemView) {
  525. collectionView.$('tbody').append(itemView.el);
  526. }
  527. });
  528. this.GridViewWithUIBindings = this.GridView.extend({
  529. template: this.GridViewWithUIBindingsTemplateFn,
  530. ui: {
  531. headersRow: 'thead tr',
  532. unfoundElement: '#unfound',
  533. itemRows: 'tbody tr'
  534. }
  535. });
  536. this.userData = [
  537. {
  538. username: 'dbailey',
  539. fullname: 'Derick Bailey'
  540. },
  541. {
  542. username: 'jbob',
  543. fullname: 'Joe Bob'
  544. }
  545. ];
  546. this.headersModel = new Backbone.Model({
  547. userHeader: 'Username',
  548. nameHeader: 'Full name'
  549. });
  550. this.userList = new this.UserCollection(this.userData);
  551. this.gridView = new this.GridViewWithUIBindings({
  552. tagName: 'table',
  553. model: this.headersModel,
  554. collection: this.userList
  555. });
  556. // We don't render the view here since we need more fine-tuned control on when the view is rendered,
  557. // specifically in the test that asserts the composite view template elements are accessible before
  558. // the collection is rendered.
  559. });
  560. describe('after the whole composite view finished rendering', function() {
  561. beforeEach(function() {
  562. this.gridView.render();
  563. });
  564. describe('accessing a ui element that belongs to the model template', function() {
  565. it('should return its jQuery selector if it can be found', function() {
  566. expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('Username');
  567. });
  568. it('should return an empty jQuery object if it cannot be found', function() {
  569. expect(this.gridView.ui.unfoundElement.length).to.equal(0);
  570. });
  571. it('should return an up-to-date selector on subsequent renders', function() {
  572. // asserting state before subsequent render
  573. expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('Username');
  574. this.headersModel.set('userHeader', 'User');
  575. this.gridView.render();
  576. expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('User');
  577. });
  578. });
  579. describe('accessing a ui element that belongs to the collection', function() {
  580. // This test makes it clear that not allowing access to the collection elements is a design decision
  581. // and not a bug.
  582. it('should return an empty jQuery object', function() {
  583. expect(this.gridView.ui.itemRows.length).to.equal(0);
  584. });
  585. });
  586. });
  587. describe('after the model finished rendering, but before the collection rendered', function() {
  588. describe('accessing a ui element that belongs to the model template', function() {
  589. beforeEach(function() {
  590. var suite = this;
  591. this.gridView.onBeforeRender = function() {
  592. suite.called = true;
  593. };
  594. this.sinon.spy(this.gridView, 'onBeforeRender');
  595. this.gridView.render();
  596. });
  597. // this test enforces that ui elements should be accessible as soon as their html was inserted
  598. // to the DOM
  599. it('should return its jQuery selector', function() {
  600. expect(this.gridView.onBeforeRender).to.have.been.called;
  601. });
  602. it('should set the username', function() {
  603. expect($(this.gridView.ui.headersRow).find('th:first-child').text()).to.equal('Username');
  604. });
  605. });
  606. });
  607. });
  608. describe('has a valid inheritance chain back to Marionette.CollectionView', function() {
  609. beforeEach(function() {
  610. this.constructor = this.sinon.spy(Marionette, 'CollectionView');
  611. this.compositeView = new Marionette.CompositeView();
  612. });
  613. it('calls the parent Marionette.CollectionViews constructor function on instantiation', function() {
  614. expect(this.constructor).to.have.been.called;
  615. });
  616. });
  617. });