PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/spec/javascripts/collectionView.spec.js

https://github.com/jdaudier/backbone.marionette
JavaScript | 1136 lines | 869 code | 258 blank | 9 comment | 21 complexity | bd4c4e314ea81d01ce06d5ab8bf239e6 MD5 | raw file
  1. describe('collection view', function() {
  2. 'use strict';
  3. beforeEach(function() {
  4. // Shared View Definitions
  5. // -----------------------
  6. this.ChildView = Backbone.Marionette.ItemView.extend({
  7. tagName: 'span',
  8. render: function() {
  9. this.$el.html(this.model.get('foo'));
  10. this.trigger('render');
  11. },
  12. onRender: function() {}
  13. });
  14. this.MockCollectionView = Backbone.Marionette.CollectionView.extend({
  15. childView: this.ChildView,
  16. onBeforeRender: function() {},
  17. onRender: function() {},
  18. onBeforeAddChild: function() {},
  19. onAddChild: function() {},
  20. onBeforeRemoveChild: function() {},
  21. onRemoveChild: function() {},
  22. onRenderCollection: function() {},
  23. onBeforeRenderCollection: function() {}
  24. });
  25. });
  26. // Collection View Specs
  27. // ---------------------
  28. describe('before rendering a collection view', function() {
  29. beforeEach(function() {
  30. this.collection = new Backbone.Collection([]);
  31. this.CollectionView = this.MockCollectionView.extend({
  32. sort: function(){ return 1; }
  33. });
  34. this.collectionView = new this.CollectionView({
  35. collection: this.collection,
  36. _sortViews: function() {}
  37. });
  38. this.onAddChildSpy = this.sinon.spy(this.collectionView, 'onAddChild');
  39. this.onRemoveChildSpy = this.sinon.spy(this.collectionView, 'onRemoveChild');
  40. this.onSortViewsSpy = this.sinon.spy(this.collectionView, '_sortViews');
  41. });
  42. it('should not add a child', function() {
  43. this.collection.push({});
  44. expect(this.onAddChildSpy).to.not.have.been.called;
  45. });
  46. it('should not add a child', function() {
  47. this.collection.reset([{}]);
  48. expect(this.onAddChildSpy).to.not.have.been.called;
  49. });
  50. it('should not remove a child', function() {
  51. var model = new Backbone.Model();
  52. this.collection.add(model);
  53. this.collection.remove(model);
  54. expect(this.onRemoveChildSpy).to.not.have.been.called;
  55. });
  56. it('should not call sort', function() {
  57. this.collection.trigger('sort');
  58. expect(this.onSortViewsSpy).to.not.have.been.called;
  59. });
  60. });
  61. describe('when rendering a collection view with no "childView" specified', function() {
  62. beforeEach(function() {
  63. this.NoChildView = Backbone.Marionette.CollectionView.extend();
  64. this.collection = new Backbone.Collection([{foo:'bar'}, {foo: 'baz'}]);
  65. this.collectionView = new this.NoChildView({
  66. collection: this.collection
  67. });
  68. });
  69. it('should throw an error saying theres not child view', function() {
  70. expect(this.collectionView.render).to.throw('A "childView" must be specified');
  71. });
  72. });
  73. describe('when rendering a collection view', function() {
  74. beforeEach(function() {
  75. this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
  76. this.childViewRender = this.sinon.stub();
  77. this.collectionView = new this.MockCollectionView({
  78. collection: this.collection
  79. });
  80. this.collectionView.on('childview:render', this.childViewRender);
  81. this.sinon.spy(this.collectionView, 'onRender');
  82. this.sinon.spy(this.collectionView, 'onBeforeAddChild');
  83. this.sinon.spy(this.collectionView, 'onAddChild');
  84. this.sinon.spy(this.collectionView, 'onBeforeRender');
  85. this.sinon.spy(this.collectionView, 'onBeforeRenderCollection');
  86. this.sinon.spy(this.collectionView, 'onRenderCollection');
  87. this.sinon.spy(this.collectionView, 'trigger');
  88. this.sinon.spy(this.collectionView, 'attachHtml');
  89. this.sinon.spy(this.collectionView.$el, 'append');
  90. this.sinon.spy(this.collectionView, 'startBuffering');
  91. this.sinon.spy(this.collectionView, 'endBuffering');
  92. this.sinon.spy(this.collectionView, 'getChildView');
  93. this.collectionView.render();
  94. });
  95. it('should only call $el.append once', function() {
  96. expect(this.collectionView.$el.append.callCount).to.equal(1);
  97. });
  98. it('should only call clear render buffer once', function() {
  99. expect(this.collectionView.endBuffering.callCount).to.equal(1);
  100. });
  101. it('should add to render buffer once for each child', function() {
  102. expect(this.collectionView.attachHtml.callCount).to.equal(2);
  103. });
  104. it('should only call onRenderCollection once', function() {
  105. expect(this.collectionView.onRenderCollection).to.have.been.calledOnce;
  106. });
  107. it('should only call onBeforeRenderCollection once', function() {
  108. expect(this.collectionView.onBeforeRenderCollection).to.have.been.calledOnce;
  109. });
  110. it('should append the html for each childView', function() {
  111. expect($(this.collectionView.$el)).to.have.$html('<span>bar</span><span>baz</span>');
  112. });
  113. it('should provide the index for each childView, when appending', function() {
  114. expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(0);
  115. });
  116. it('should reference each of the rendered view children', function() {
  117. expect(_.size(this.collectionView.children)).to.equal(2);
  118. });
  119. it('should call "onBeforeRender" before rendering', function() {
  120. expect(this.collectionView.onBeforeRender).to.have.been.called;
  121. });
  122. it('should call "onRender" after rendering', function() {
  123. expect(this.collectionView.onRender).to.have.been.called;
  124. });
  125. it('should trigger a "before:render" event', function() {
  126. expect(this.collectionView.trigger).to.have.been.calledWith('before:render', this.collectionView);
  127. });
  128. it('should trigger a "before:render:collection" event', function() {
  129. expect(this.collectionView.trigger).to.have.been.calledWith('before:render:collection', this.collectionView);
  130. });
  131. it('should trigger a "render:collection" event', function() {
  132. expect(this.collectionView.trigger).to.have.been.calledWith('render:collection', this.collectionView);
  133. });
  134. it('should trigger a "render" event', function() {
  135. expect(this.collectionView.trigger).to.have.been.calledWith('render', this.collectionView);
  136. });
  137. it('should call "onBeforeAddChild" for each childView instance', function() {
  138. var v1 = this.collectionView.children.findByIndex(0);
  139. var v2 = this.collectionView.children.findByIndex(1);
  140. expect(this.collectionView.onBeforeAddChild).to.have.been.calledWith(v1);
  141. expect(this.collectionView.onBeforeAddChild).to.have.been.calledWith(v2);
  142. });
  143. it('should call "onAddChild" for each childView instance', function() {
  144. var v1 = this.collectionView.children.findByIndex(0);
  145. var v2 = this.collectionView.children.findByIndex(1);
  146. expect(this.collectionView.onAddChild).to.have.been.calledWith(v1);
  147. expect(this.collectionView.onAddChild).to.have.been.calledWith(v2);
  148. });
  149. it('should call "onBeforeAddChild" for all childView instances', function() {
  150. expect(this.collectionView.onBeforeAddChild.callCount).to.equal(2);
  151. });
  152. it('should call "onAddChild" for all childView instances', function() {
  153. expect(this.collectionView.onAddChild.callCount).to.equal(2);
  154. });
  155. it('should trigger "childview:render" for each item in the collection', function() {
  156. expect(this.childViewRender.callCount).to.equal(2);
  157. });
  158. it('should call "getChildView" for each item in the collection', function() {
  159. expect(this.collectionView.getChildView).to.have.been.calledTwice.
  160. and.calledWith(this.collection.models[0]).
  161. and.calledWith(this.collection.models[1]);
  162. });
  163. });
  164. describe('when rendering a collection view and accessing children via the DOM', function() {
  165. beforeEach(function() {
  166. var suite = this;
  167. this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
  168. this.collectionView = new this.MockCollectionView({
  169. collection: this.collection
  170. });
  171. this.sinon.stub(this.collectionView, 'onRenderCollection', function() {
  172. suite.onRenderCollectionHTML = this.el.innerHTML;
  173. });
  174. this.collectionView.render();
  175. });
  176. it('should find the expected number of childen', function() {
  177. expect(this.onRenderCollectionHTML).to.equal("<span>bar</span><span>baz</span>");
  178. });
  179. });
  180. describe('when rendering a collection view without a collection', function() {
  181. beforeEach(function() {
  182. this.collectionView = new this.MockCollectionView();
  183. this.sinon.spy(this.collectionView, 'onRender');
  184. this.sinon.spy(this.collectionView, 'onBeforeRender');
  185. this.sinon.spy(this.collectionView, 'trigger');
  186. this.collectionView.render();
  187. });
  188. it('should not append any html', function() {
  189. expect($(this.collectionView.$el)).not.to.have.$html('<span>bar</span><span>baz</span>');
  190. });
  191. it('should not reference any view children', function() {
  192. expect(this.collectionView.children.length).to.equal(0);
  193. });
  194. });
  195. describe('when rendering a childView', function() {
  196. beforeEach(function() {
  197. this.collection = new Backbone.Collection([{foo: 'bar'}]);
  198. this.collectionView = new Marionette.CollectionView({
  199. childView: this.ChildView,
  200. collection: this.collection
  201. });
  202. this.collectionView.render();
  203. this.childView = this.collectionView.children.first();
  204. this.sinon.spy(this.childView, 'render');
  205. this.sinon.spy(this.collectionView, 'renderChildView');
  206. this.collectionView.renderChildView(this.childView);
  207. });
  208. it('should call "render" on the childView', function() {
  209. expect(this.childView.render).to.have.been.calledOnce;
  210. });
  211. it('should return the childView', function() {
  212. expect(this.collectionView.renderChildView).to.have.returned(this.childView);
  213. });
  214. });
  215. describe('when a model is added to the collection', function() {
  216. beforeEach(function() {
  217. this.collection = new Backbone.Collection();
  218. this.collectionView = new this.MockCollectionView({
  219. childView: this.ChildView,
  220. collection: this.collection
  221. });
  222. this.collectionView.render();
  223. this.childViewRender = this.sinon.stub();
  224. this.collectionView.on('childview:render', this.childViewRender);
  225. this.sinon.spy(this.collectionView, 'attachHtml');
  226. this.model = new Backbone.Model({foo: 'bar'});
  227. this.collection.add(this.model);
  228. });
  229. it('should add the model to the list', function() {
  230. expect(_.size(this.collectionView.children)).to.equal(1);
  231. });
  232. it('should render the model in to the DOM', function() {
  233. expect($(this.collectionView.$el)).to.contain.$text('bar');
  234. });
  235. it('should provide the index for each childView, when appending', function() {
  236. expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(0);
  237. });
  238. it('should trigger the childview:render event from the collectionView', function() {
  239. expect(this.childViewRender).to.have.been.called;
  240. });
  241. });
  242. describe('when a model is added to a non-empty collection', function() {
  243. beforeEach(function() {
  244. this.collection = new Backbone.Collection({foo: 'bar'});
  245. this.collectionView = new this.MockCollectionView({
  246. childView: this.ChildView,
  247. collection: this.collection
  248. });
  249. this.collectionView.render();
  250. this.childViewRender = this.sinon.stub();
  251. this.collectionView.on('childview:render', this.childViewRender);
  252. this.sinon.spy(this.collectionView, 'attachHtml');
  253. this.model = new Backbone.Model({foo: 'baz'});
  254. this.collection.add(this.model);
  255. });
  256. it('should add the model to the list', function() {
  257. expect(_.size(this.collectionView.children)).to.equal(2);
  258. });
  259. it('should render the model in to the DOM', function() {
  260. expect($(this.collectionView.$el)).to.contain.$text('barbaz');
  261. });
  262. it('should provide the index for each child view, when appending', function() {
  263. expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(1);
  264. });
  265. it('should trigger the childview:render event from the collectionView', function() {
  266. expect(this.childViewRender).to.have.been.called;
  267. });
  268. });
  269. describe('when providing a custom render that adds children, without a collection object to use, and removing a child', function() {
  270. beforeEach(function() {
  271. var suite = this;
  272. this.model = new Backbone.Model({foo: 'bar'});
  273. this.EmptyView = Backbone.Marionette.ItemView.extend({
  274. render: function() {}
  275. });
  276. this.CollectionView = Backbone.Marionette.CollectionView.extend({
  277. childView: this.ChildView,
  278. emptyView: this.EmptyView,
  279. onBeforeRenderEmpty: function() {},
  280. onRenderEmpty: function() {},
  281. render: function() {
  282. var ChildView = this.getChildView();
  283. this.addChild(suite.model, ChildView, 0);
  284. }
  285. });
  286. this.collectionView = new this.CollectionView({});
  287. this.collectionView.render();
  288. this.childView = this.collectionView.children.findByIndex(0);
  289. this.beforeRenderSpy = this.sinon.spy(this.collectionView, 'onBeforeRenderEmpty');
  290. this.renderSpy = this.sinon.spy(this.collectionView, 'onRenderEmpty');
  291. this.sinon.spy(this.childView, 'destroy');
  292. this.sinon.spy(this.EmptyView.prototype, 'render');
  293. this.collectionView._onCollectionRemove(this.model);
  294. });
  295. it('should destroy the models view', function() {
  296. expect(this.childView.destroy).to.have.been.called;
  297. });
  298. it('should show the empty view', function() {
  299. expect(this.EmptyView.prototype.render.callCount).to.equal(1);
  300. });
  301. it('should call "onBeforeRenderEmpty"', function() {
  302. expect(this.beforeRenderSpy).to.have.been.called;
  303. });
  304. it('should call "onRenderEmpty"', function() {
  305. expect(this.renderSpy).to.have.been.called;
  306. });
  307. });
  308. describe('when a model is removed from the collection', function() {
  309. beforeEach(function() {
  310. this.model = new Backbone.Model({foo: 'bar'});
  311. this.collection = new Backbone.Collection();
  312. this.collection.add(this.model);
  313. this.collectionView = new this.MockCollectionView({
  314. childView: this.ChildView,
  315. collection: this.collection
  316. });
  317. this.collectionView.render();
  318. this.childView = this.collectionView.children.findByIndex(0);
  319. this.sinon.spy(this.childView, 'destroy');
  320. this.onBeforeRemoveChildSpy = this.sinon.spy(this.collectionView, 'onBeforeRemoveChild');
  321. this.onRemoveChildSpy = this.sinon.spy(this.collectionView, 'onRemoveChild');
  322. this.collection.remove(this.model);
  323. });
  324. it('should destroy the models view', function() {
  325. expect(this.childView.destroy).to.have.been.called;
  326. });
  327. it('should remove the model-views HTML', function() {
  328. expect($(this.collectionView.$el).children().length).to.equal(0);
  329. });
  330. it('should execute onBeforeRemoveChild', function() {
  331. expect(this.onBeforeRemoveChildSpy).to.have.been.calledOnce;
  332. });
  333. it('should pass the removed view to onBeforeRemoveChild', function() {
  334. expect(this.onBeforeRemoveChildSpy).to.have.been.calledWithExactly(this.childView);
  335. });
  336. it('should execute onRemoveChild', function() {
  337. expect(this.onRemoveChildSpy).to.have.been.calledOnce;
  338. });
  339. it('should pass the removed view to _onCollectionRemove', function() {
  340. expect(this.onRemoveChildSpy).to.have.been.calledWithExactly(this.childView);
  341. });
  342. it('should execute onBeforeRemoveChild before _onCollectionRemove', function() {
  343. expect(this.onBeforeRemoveChildSpy).to.have.been.calledBefore(this.onRemoveChildSpy);
  344. });
  345. });
  346. describe('when destroying a collection view', function() {
  347. beforeEach(function() {
  348. this.EventedView = Backbone.Marionette.CollectionView.extend({
  349. childView: this.ChildView,
  350. someCallback: function() {},
  351. onBeforeDestroy: function() {},
  352. onDestroy: function() {}
  353. });
  354. this.destroyHandler = this.sinon.stub();
  355. this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
  356. this.collectionView = new this.EventedView({
  357. template: '#itemTemplate',
  358. collection: this.collection
  359. });
  360. this.collectionView.someItemViewCallback = function() {};
  361. this.collectionView.render();
  362. this.childModel = this.collection.at(0);
  363. this.childView = this.collectionView.children.findByIndex(0);
  364. this.collectionView.listenTo(this.collection, 'foo', this.collectionView.someCallback);
  365. this.collectionView.listenTo(this.collectionView, 'item:foo', this.collectionView.someItemViewCallback);
  366. this.sinon.spy(this.childView, 'destroy');
  367. this.sinon.spy(this.collectionView, '_onCollectionRemove');
  368. this.sinon.spy(this.collectionView, 'stopListening');
  369. this.sinon.spy(this.collectionView, 'remove');
  370. this.sinon.spy(this.collectionView, 'someCallback');
  371. this.sinon.spy(this.collectionView, 'someItemViewCallback');
  372. this.sinon.spy(this.collectionView, 'destroy');
  373. this.sinon.spy(this.collectionView, 'onDestroy');
  374. this.sinon.spy(this.collectionView, 'onBeforeDestroy');
  375. this.sinon.spy(this.collectionView, 'trigger');
  376. this.collectionView.bind('destroy:collection', this.destroyHandler);
  377. this.collectionView.destroy();
  378. this.childView.trigger('foo');
  379. this.collection.trigger('foo');
  380. this.collection.remove(this.childModel);
  381. });
  382. it('should destroy all of the child views', function() {
  383. expect(this.childView.destroy).to.have.been.called;
  384. });
  385. it('should unbind all the listenTo events', function() {
  386. expect(this.collectionView.stopListening).to.have.been.called;
  387. });
  388. it('should unbind all collection events for the view', function() {
  389. expect(this.collectionView.someCallback).not.to.have.been.called;
  390. });
  391. it('should unbind all item-view events for the view', function() {
  392. expect(this.collectionView.someItemViewCallback).not.to.have.been.called;
  393. });
  394. it('should not retain any references to its children', function() {
  395. expect(_.size(this.collectionView.children)).to.equal(0);
  396. });
  397. it('should unbind any listener to custom view events', function() {
  398. expect(this.collectionView.stopListening).to.have.been.called;
  399. });
  400. it('should remove the views EL from the DOM', function() {
  401. expect(this.collectionView.remove).to.have.been.called;
  402. });
  403. it('should call "onDestroy" if provided', function() {
  404. expect(this.collectionView.onDestroy).to.have.been.called;
  405. });
  406. it('should call "onBeforeDestroy" if provided', function() {
  407. expect(this.collectionView.onBeforeDestroy).to.have.been.called;
  408. });
  409. it('should trigger a "before:destroy" event', function() {
  410. expect(this.collectionView.trigger).to.have.been.calledWith('before:destroy:collection');
  411. });
  412. it('should trigger a "destroy"', function() {
  413. expect(this.collectionView.trigger).to.have.been.calledWith('destroy:collection');
  414. });
  415. it('should call the handlers add to the destroyed event', function() {
  416. expect(this.destroyHandler).to.have.been.called;
  417. });
  418. it('should throw an error saying the views been destroyed if render is attempted again', function() {
  419. expect(this.collectionView.render).to.throw('View (cid: "' + this.collectionView.cid +
  420. '") has already been destroyed and cannot be used.');
  421. });
  422. it('should return the collection view', function() {
  423. expect(this.collectionView.destroy).to.have.returned(this.collectionView);
  424. });
  425. });
  426. describe('when removing a childView that does not have a "destroy" method', function() {
  427. beforeEach(function() {
  428. this.collectionView = new Marionette.CollectionView({
  429. childView: Backbone.View,
  430. collection: new Backbone.Collection([{id: 1}])
  431. });
  432. this.collectionView.render();
  433. this.childView = this.collectionView.children.findByIndex(0);
  434. this.sinon.spy(this.childView, 'remove');
  435. this.sinon.spy(this.collectionView, 'removeChildView');
  436. this.collectionView.removeChildView(this.childView);
  437. });
  438. it('should call the "remove" method', function() {
  439. expect(this.childView.remove).to.have.been.called;
  440. });
  441. it('should return the childView', function() {
  442. expect(this.collectionView.removeChildView).to.have.returned(this.childView);
  443. });
  444. });
  445. describe('when destroying all children', function() {
  446. beforeEach(function() {
  447. this.collectionView = new Marionette.CollectionView({
  448. childView: Backbone.View,
  449. collection: new Backbone.Collection([{id: 1}, {id: 2}])
  450. });
  451. this.collectionView.render();
  452. this.childView0 = this.collectionView.children.findByIndex(0);
  453. this.sinon.spy(this.childView0, 'remove');
  454. this.childView1 = this.collectionView.children.findByIndex(1);
  455. this.sinon.spy(this.childView1, 'remove');
  456. this.childrenViews = this.collectionView.children.map(_.identity);
  457. this.sinon.spy(this.collectionView, 'destroyChildren');
  458. this.collectionView.destroyChildren();
  459. });
  460. it('should call the "remove" method on each child', function() {
  461. expect(this.childView0.remove).to.have.been.called;
  462. expect(this.childView1.remove).to.have.been.called;
  463. });
  464. it('should return the child views', function() {
  465. expect(this.collectionView.destroyChildren).to.have.returned(this.childrenViews);
  466. });
  467. });
  468. describe('when override attachHtml', function() {
  469. beforeEach(function() {
  470. this.PrependHtmlView = Backbone.Marionette.CollectionView.extend({
  471. childView: this.ChildView,
  472. attachHtml: function(collectionView, childView) {
  473. collectionView.$el.prepend(childView.el);
  474. }
  475. });
  476. this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
  477. this.collectionView = new this.PrependHtmlView({
  478. collection: this.collection
  479. });
  480. this.collectionView.render();
  481. });
  482. it('should append via the overridden method', function() {
  483. expect(this.collectionView.$el).to.contain.$html('<span>baz</span><span>bar</span>');
  484. });
  485. });
  486. describe('when a child view triggers an event', function() {
  487. beforeEach(function() {
  488. this.someEventSpy = this.sinon.stub();
  489. this.model = new Backbone.Model({foo: 'bar'});
  490. this.collection = new Backbone.Collection([this.model]);
  491. this.collectionView = new this.MockCollectionView({collection: this.collection});
  492. this.collectionView.on('childview:some:event', this.someEventSpy);
  493. this.collectionView.render();
  494. this.sinon.spy(this.collectionView, 'trigger');
  495. this.childView = this.collectionView.children.findByIndex(0);
  496. this.childView.trigger('some:event', 'test', this.model);
  497. });
  498. it('should bubble up through the parent collection view', function() {
  499. expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
  500. });
  501. it('should provide the child view that triggered the event, including other relevant parameters', function() {
  502. expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
  503. });
  504. });
  505. describe('when configuring a custom childViewEventPrefix', function() {
  506. beforeEach(function() {
  507. this.CollectionView = this.MockCollectionView.extend({
  508. childViewEventPrefix: 'myPrefix'
  509. });
  510. this.someEventSpy = this.sinon.stub();
  511. this.model = new Backbone.Model({foo: 'bar'});
  512. this.collection = new Backbone.Collection([this.model]);
  513. this.collectionView = new this.CollectionView({collection: this.collection});
  514. this.collectionView.on('myPrefix:some:event', this.someEventSpy);
  515. this.collectionView.render();
  516. this.sinon.spy(this.collectionView, 'trigger');
  517. this.childView = this.collectionView.children.findByIndex(0);
  518. this.childView.trigger('some:event', 'test', this.model);
  519. });
  520. it('should bubble up through the parent collection view', function() {
  521. expect(this.collectionView.trigger).to.have.been.calledWith('myPrefix:some:event', this.childView, 'test', this.model);
  522. });
  523. it('should provide the child view that triggered the event, including other relevant parameters', function() {
  524. expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
  525. });
  526. });
  527. describe('when a child view triggers the default', function() {
  528. beforeEach(function() {
  529. this.model = new Backbone.Model({foo: 'bar'});
  530. this.collection = new Backbone.Collection([this.model]);
  531. this.collectionView = new this.MockCollectionView({
  532. childView: Backbone.Marionette.ItemView.extend({
  533. template: function() { return '<%= foo %>'; }
  534. }),
  535. collection: this.collection
  536. });
  537. });
  538. describe('render events', function() {
  539. beforeEach(function() {
  540. this.beforeSpy = this.sinon.stub();
  541. this.renderSpy = this.sinon.stub();
  542. this.collectionView.on('childview:before:render', this.beforeSpy);
  543. this.collectionView.on('childview:render', this.renderSpy);
  544. this.collectionView.render();
  545. this.childView = this.collectionView.children.findByIndex(0);
  546. });
  547. it('should bubble up through the parent collection view', function() {
  548. // As odd as it seems, the events are triggered with two arguments,
  549. // the first being the child view which triggered the event
  550. // and the second being the event's owner. It just so happens to be the
  551. // same view.
  552. expect(this.beforeSpy).to.have.been.calledWith(this.childView, this.childView);
  553. expect(this.renderSpy).to.have.been.calledWith(this.childView, this.childView);
  554. });
  555. });
  556. describe('destroy events', function() {
  557. beforeEach(function() {
  558. this.beforeSpy = this.sinon.stub();
  559. this.destroySpy = this.sinon.stub();
  560. this.collectionView.on('childview:before:destroy', this.beforeSpy);
  561. this.collectionView.on('childview:destroy', this.destroySpy);
  562. this.collectionView.render();
  563. this.childView = this.collectionView.children.findByIndex(0);
  564. this.collectionView.destroy();
  565. });
  566. it('should bubble up through the parent collection view', function() {
  567. expect(this.beforeSpy).to.have.been.calledWith(this.childView);
  568. expect(this.destroySpy).to.have.been.calledWith(this.childView);
  569. });
  570. });
  571. });
  572. describe('when a child view is removed from a collection view', function() {
  573. beforeEach(function() {
  574. this.model = new Backbone.Model({foo: 'bar'});
  575. this.collection = new Backbone.Collection([this.model]);
  576. this.collectionView = new this.MockCollectionView({
  577. template: '#itemTemplate',
  578. collection: this.collection
  579. });
  580. this.collectionView.render();
  581. this.childView = this.collectionView.children[this.model.cid];
  582. this.collection.remove(this.model);
  583. });
  584. it('should not retain any bindings to this view', function() {
  585. var suite = this;
  586. var bindings = this.collectionView.bindings || {};
  587. expect(_.any(bindings, function(binding) {
  588. return binding.obj === suite.childView;
  589. })).to.be.false;
  590. });
  591. it('should not retain any references to this view', function() {
  592. expect(_.size(this.collectionView.children)).to.equal(0);
  593. });
  594. });
  595. describe('when the collection of a collection view is reset', function() {
  596. beforeEach(function() {
  597. this.model = new Backbone.Model({foo: 'bar'});
  598. this.collection = new Backbone.Collection([ this.model ]);
  599. this.collectionView = new this.MockCollectionView({
  600. template: '#itemTemplate',
  601. collection: this.collection
  602. });
  603. this.collectionView.render();
  604. this.childView = this.collectionView.children[this.model.cid];
  605. this.collection.reset();
  606. });
  607. it('should not retain any references to the previous views', function() {
  608. expect(_.size(this.collectionView.children)).to.equal(0);
  609. });
  610. it('should not retain any bindings to the previous views', function() {
  611. var suite = this;
  612. var bindings = this.collectionView.bindings || {};
  613. expect(_.any(bindings, function(binding) {
  614. return binding.obj === suite.childView;
  615. })).to.be.false;
  616. });
  617. });
  618. describe('when a child view is added to a collection view, after the collection view has been shown', function() {
  619. beforeEach(function() {
  620. this.ChildView = Backbone.Marionette.ItemView.extend({
  621. onShow: function() {},
  622. onDomRefresh: function() {},
  623. onRender: function() {},
  624. render: function() {
  625. this.trigger('render');
  626. }
  627. });
  628. this.CollectionView = Backbone.Marionette.CollectionView.extend({
  629. childView: this.ChildView,
  630. onShow: function() {}
  631. });
  632. this.sinon.spy(this.ChildView.prototype, 'onShow');
  633. this.sinon.spy(this.ChildView.prototype, 'onDomRefresh');
  634. this.model1 = new Backbone.Model();
  635. this.model2 = new Backbone.Model();
  636. this.collection = new Backbone.Collection([ this.model1 ]);
  637. this.collectionView = new this.CollectionView({
  638. collection: this.collection
  639. });
  640. $('body').append(this.collectionView.el);
  641. this.collectionView.render();
  642. this.collectionView.onShow();
  643. this.collectionView.trigger('show');
  644. this.sinon.spy(this.collectionView, 'attachBuffer');
  645. this.sinon.spy(this.collectionView, 'getChildView');
  646. this.collection.add(this.model2);
  647. this.view = this.collectionView.children.findByIndex(1);
  648. });
  649. it('should not use the render buffer', function() {
  650. expect(this.collectionView.attachBuffer).not.to.have.been.called;
  651. });
  652. it('should call the "onShow" method of the child view', function() {
  653. expect(this.ChildView.prototype.onShow).to.have.been.called;
  654. });
  655. it('should call the childs "onShow" method with itself as the context', function() {
  656. expect(this.ChildView.prototype.onShow).to.have.been.calledOn(this.view);
  657. });
  658. it('should call the childs "onDomRefresh" method with itself as the context', function() {
  659. expect(this.ChildView.prototype.onDomRefresh).to.have.been.called;
  660. });
  661. it('should call "getChildView" with the new model', function() {
  662. expect(this.collectionView.getChildView).to.have.been.calledWith(this.model2);
  663. });
  664. });
  665. describe('when setting an childView in the constructor options', function() {
  666. beforeEach(function() {
  667. this.ItemView = Marionette.ItemView.extend({
  668. template: function() {},
  669. MyItemView: true
  670. });
  671. this.collection = new Backbone.Collection([{a: 'b'}]);
  672. this.collectionView = new Marionette.CollectionView({
  673. childView: this.ItemView,
  674. collection: this.collection
  675. });
  676. this.collectionView.render();
  677. this.itemView = this.collectionView.children.findByModel(this.collection.at(0));
  678. });
  679. it('should use the specified childView for each item', function() {
  680. expect(this.itemView.MyItemView).to.be.true;
  681. });
  682. });
  683. describe('when calling childEvents via an childEvents method', function() {
  684. beforeEach(function() {
  685. this.CollectionView = this.MockCollectionView.extend({
  686. childEvents: function() {
  687. return {
  688. 'some:event': 'someEvent'
  689. };
  690. }
  691. });
  692. this.someEventSpy = this.sinon.stub();
  693. this.model = new Backbone.Model({foo: 'bar'});
  694. this.collection = new Backbone.Collection([this.model]);
  695. this.collectionView = new this.CollectionView({collection: this.collection});
  696. this.collectionView.someEvent = this.someEventSpy;
  697. this.collectionView.render();
  698. this.sinon.spy(this.collectionView, 'trigger');
  699. this.childView = this.collectionView.children.findByIndex(0);
  700. this.childView.trigger('some:event', 'test', this.model);
  701. });
  702. it('should bubble up through the parent collection view', function() {
  703. expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
  704. });
  705. it('should provide the child view that triggered the event, including other relevant parameters', function() {
  706. expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
  707. });
  708. });
  709. describe('when calling childEvents via the childEvents hash', function() {
  710. beforeEach(function() {
  711. this.onSomeEventSpy = this.sinon.stub();
  712. this.CollectionView = this.MockCollectionView.extend({
  713. childEvents: {
  714. 'some:event': this.onSomeEventSpy
  715. }
  716. });
  717. this.model = new Backbone.Model({foo: 'bar'});
  718. this.collection = new Backbone.Collection([this.model]);
  719. this.collectionView = new this.CollectionView({collection: this.collection});
  720. this.collectionView.render();
  721. this.sinon.spy(this.collectionView, 'trigger');
  722. this.childView = this.collectionView.children.findByIndex(0);
  723. this.childView.trigger('some:event', 'test', this.model);
  724. });
  725. it('should bubble up through the parent collection view', function() {
  726. expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
  727. });
  728. it('should provide the child view that triggered the event, including other relevant parameters', function() {
  729. expect(this.onSomeEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
  730. });
  731. });
  732. describe('when calling childEvents via the childEvents hash with a string of the function name', function() {
  733. beforeEach(function() {
  734. this.CollectionView = this.MockCollectionView.extend({
  735. childEvents: {
  736. 'some:event': 'someEvent'
  737. }
  738. });
  739. this.someEventSpy = this.sinon.stub();
  740. this.model = new Backbone.Model({foo: 'bar'});
  741. this.collection = new Backbone.Collection([this.model]);
  742. this.collectionView = new this.CollectionView({collection: this.collection});
  743. this.collectionView.someEvent = this.someEventSpy;
  744. this.collectionView.render();
  745. this.sinon.spy(this.collectionView, 'trigger');
  746. this.childView = this.collectionView.children.findByIndex(0);
  747. this.childView.trigger('some:event', 'test', this.model);
  748. });
  749. it('should bubble up through the parent collection view', function() {
  750. expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
  751. });
  752. it('should provide the child view that triggered the event, including other relevant parameters', function() {
  753. expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
  754. });
  755. });
  756. describe('calling childEvents via the childEvents hash with a string of a nonexistent function name', function() {
  757. beforeEach(function() {
  758. this.CollectionView = Marionette.CollectionView.extend({
  759. childView: this.ChildView,
  760. childEvents: {
  761. 'render': 'nonexistentFn'
  762. }
  763. });
  764. this.collectionView = new this.CollectionView({
  765. collection: (new Backbone.Collection([{}]))
  766. });
  767. this.collectionView.render();
  768. });
  769. it('should not break', function() {
  770. // Intentionally left blank
  771. });
  772. });
  773. describe('has a valid inheritance chain back to Marionette.View', function() {
  774. beforeEach(function() {
  775. this.constructor = this.sinon.spy(Marionette, 'View');
  776. this.collectionView = new Marionette.CollectionView();
  777. });
  778. it('calls the parent Marionette.Views constructor function on instantiation', function() {
  779. expect(this.constructor).to.have.been.called;
  780. });
  781. });
  782. describe('when a collection is reset child views should not be shown until the buffering is over', function() {
  783. beforeEach(function() {
  784. var suite = this;
  785. this.ItemView = Marionette.ItemView.extend({
  786. template: _.template('<div>hi mom</div>'),
  787. onShow: function() {
  788. suite.isBuffering = suite.collectionView.isBuffering;
  789. }
  790. });
  791. this.CollectionView = Marionette.CollectionView.extend({
  792. childView: this.ItemView
  793. });
  794. this.isBuffering = null;
  795. this.collection = new Backbone.Collection([{}]);
  796. this.collectionView = new this.CollectionView({collection: this.collection});
  797. this.collectionView.render().trigger('show');
  798. });
  799. it('collectionView should not be buffering on childView show', function() {
  800. expect(this.isBuffering).to.be.false;
  801. });
  802. it('collectionView should not be buffering after reset on childView show', function() {
  803. this.isBuffering = undefined;
  804. this.collection.reset([{}]);
  805. expect(this.isBuffering).to.be.false;
  806. });
  807. describe('child view show events', function() {
  808. beforeEach(function() {
  809. var suite = this;
  810. this.showCalled = false;
  811. this.ItemView.prototype.onShow = function() { suite.showCalled = true; };
  812. });
  813. it('collectionView should trigger the show events when the buffer is inserted and the view has been shown', function() {
  814. this.collection.reset([{}]);
  815. expect(this.showCalled).to.equal(true);
  816. });
  817. it('collectionView should not trigger the show events if the view has not been shown', function() {
  818. this.collectionView = new this.CollectionView({collection: this.collection});
  819. this.collectionView.render();
  820. expect(this.showCalled).to.equal(false);
  821. });
  822. });
  823. });
  824. describe('when a collection view is not rendered', function() {
  825. beforeEach(function() {
  826. var suite = this;
  827. this.Model = Backbone.Model.extend({});
  828. this.Collection = Backbone.Collection.extend({model: this.Model});
  829. this.CollectionView = Backbone.Marionette.CollectionView.extend({
  830. childView: this.ChildView,
  831. tagName: 'ul'
  832. });
  833. this.addModel = function() {
  834. suite.collection.add(suite.model2);
  835. };
  836. this.removeModel = function() {
  837. suite.collection.remove(suite.model1);
  838. };
  839. this.resetCollection = function() {
  840. suite.collection.reset([suite.model1, suite.model2]);
  841. };
  842. this.sync = function() {
  843. suite.collection.trigger('sync');
  844. };
  845. this.model1 = new this.Model({monkey: 'island'});
  846. this.model2 = new this.Model({lechuck: 'tours'});
  847. this.collection = new this.Collection([this.model1]);
  848. this.collectionView = new this.CollectionView({
  849. collection: this.collection
  850. });
  851. });
  852. it('should not fail when adding models to an unrendered collectionView', function() {
  853. expect(this.addModel).not.to.throw;
  854. });
  855. it('should not fail when an item is removed from an unrendered collectionView', function() {
  856. expect(this.removeModel).not.to.throw;
  857. });
  858. it('should not fail when a collection is reset on an unrendered collectionView', function() {
  859. expect(this.resetCollection).not.to.throw;
  860. });
  861. it('should not fail when a collection is synced on an unrendered collectionView', function() {
  862. expect(this.sync).not.to.throw;
  863. });
  864. });
  865. describe('when returning the view from addChild', function() {
  866. beforeEach(function() {
  867. this.model = new Backbone.Model({foo: 'bar'});
  868. this.CollectionView = Backbone.Marionette.CollectionView.extend({
  869. childView: this.ChildView
  870. });
  871. this.collectionView = new this.CollectionView();
  872. this.childView = this.collectionView.addChild(this.model, this.ChildView, 0);
  873. });
  874. it('should return the child view for the model', function() {
  875. expect(this.childView.$el).to.contain.$text('bar');
  876. });
  877. });
  878. });