/spec/javascripts/collectionView.spec.js

https://bitbucket.org/stevepict/backbone.marionette · JavaScript · 644 lines · 490 code · 154 blank · 0 comment · 0 complexity · 4d6bcb65b91c171f84aec652d3691c6c MD5 · raw file

  1. describe("collection view", function(){
  2. var Model = Backbone.Model.extend({});
  3. var Collection = Backbone.Collection.extend({
  4. model: Model
  5. });
  6. var ItemView = Backbone.Marionette.ItemView.extend({
  7. tagName: "span",
  8. render: function(){
  9. this.$el.html(this.model.get("foo"));
  10. },
  11. onRender: function(){}
  12. });
  13. var CollectionView = Backbone.Marionette.CollectionView.extend({
  14. itemView: ItemView,
  15. beforeRender: function(){},
  16. onRender: function(){},
  17. onItemAdded: function(view){}
  18. });
  19. var EventedView = Backbone.Marionette.CollectionView.extend({
  20. itemView: ItemView,
  21. someCallback: function(){ },
  22. beforeClose: function(){},
  23. onClose: function(){ }
  24. });
  25. var PrependHtmlView = Backbone.Marionette.CollectionView.extend({
  26. itemView: ItemView,
  27. appendHtml: function(collectionView, itemView){
  28. collectionView.$el.prepend(itemView.el);
  29. }
  30. });
  31. describe("when rendering a collection view with no `itemView` specified", function(){
  32. var NoItemView = Backbone.Marionette.CollectionView.extend({
  33. });
  34. var collectionView;
  35. beforeEach(function(){
  36. var collection = new Collection([{foo: "bar"}, {foo: "baz"}]);
  37. collectionView = new NoItemView({
  38. collection: collection
  39. });
  40. });
  41. it("should throw an error saying there's not item view", function(){
  42. expect(function(){collectionView.render()}).toThrow("An `itemView` must be specified");
  43. });
  44. });
  45. describe("when rendering a collection view", function(){
  46. var collection = new Collection([{foo: "bar"}, {foo: "baz"}]);
  47. var collectionView;
  48. beforeEach(function(){
  49. collectionView = new CollectionView({
  50. collection: collection
  51. });
  52. spyOn(collectionView, "onRender").andCallThrough();
  53. spyOn(collectionView, "onItemAdded").andCallThrough();
  54. spyOn(collectionView, "beforeRender").andCallThrough();
  55. spyOn(collectionView, "trigger").andCallThrough();
  56. spyOn(collectionView, "appendHtml").andCallThrough();
  57. collectionView.render();
  58. });
  59. it("should append the html for each itemView", function(){
  60. expect($(collectionView.$el)).toHaveHtml("<span>bar</span><span>baz</span>");
  61. });
  62. it("should provide the index for each itemView, when appending", function(){
  63. expect(collectionView.appendHtml.calls[0].args[2]).toBe(0);
  64. });
  65. it("should reference each of the rendered view items", function(){
  66. expect(_.size(collectionView.children)).toBe(2);
  67. });
  68. it("should call 'beforeRender' before rendering", function(){
  69. expect(collectionView.beforeRender).toHaveBeenCalled();
  70. });
  71. it("should call 'onRender' after rendering", function(){
  72. expect(collectionView.onRender).toHaveBeenCalled();
  73. });
  74. it("should trigger a 'before:render' event", function(){
  75. expect(collectionView.trigger).toHaveBeenCalledWith("before:render", collectionView);
  76. });
  77. it("should trigger a 'collection:before:render' event", function(){
  78. expect(collectionView.trigger).toHaveBeenCalledWith("collection:before:render", collectionView);
  79. });
  80. it("should trigger a 'collection:rendered' event", function(){
  81. expect(collectionView.trigger).toHaveBeenCalledWith("collection:rendered", collectionView);
  82. });
  83. it("should trigger a 'render' event", function(){
  84. expect(collectionView.trigger).toHaveBeenCalledWith("render", collectionView);
  85. });
  86. it("should call `onItemAdded` for each itemView instance", function(){
  87. var views = _.values(collectionView.children);
  88. var v1 = views[0];
  89. var v2 = views[1];
  90. expect(collectionView.onItemAdded).toHaveBeenCalledWith(v1);
  91. expect(collectionView.onItemAdded).toHaveBeenCalledWith(v2);
  92. });
  93. it("should call `onItemAdded` for all itemView instances", function(){
  94. expect(collectionView.onItemAdded.callCount).toBe(2);
  95. });
  96. });
  97. describe("when rendering and an 'itemViewOptions' is provided", function(){
  98. var CollectionView = Backbone.Marionette.CollectionView.extend({
  99. itemView: ItemView,
  100. itemViewOptions: {
  101. foo: "bar"
  102. }
  103. });
  104. var collection = new Collection([{foo: "bar"}]);
  105. var collectionView, view;
  106. beforeEach(function(){
  107. collectionView = new CollectionView({
  108. collection: collection
  109. });
  110. collectionView.render();
  111. view = _.values(collectionView.children)[0];
  112. });
  113. it("should pass the options to every view instance", function(){
  114. expect(view.options.hasOwnProperty("foo")).toBe(true);
  115. });
  116. });
  117. describe("when rendering and an 'itemViewOptions' is provided as a function", function(){
  118. var CollectionView = Backbone.Marionette.CollectionView.extend({
  119. itemView: ItemView,
  120. itemViewOptions: function(){
  121. return {
  122. foo: "bar"
  123. };
  124. }
  125. });
  126. var collection = new Collection([{foo: "bar"}]);
  127. var collectionView, view;
  128. beforeEach(function(){
  129. collectionView = new CollectionView({
  130. collection: collection
  131. });
  132. collectionView.render();
  133. view = _.values(collectionView.children)[0];
  134. });
  135. it("should pass the options to every view instance", function(){
  136. expect(view.options.hasOwnProperty("foo")).toBe(true);
  137. });
  138. });
  139. describe("when rendering a collection view without a collection", function(){
  140. var collectionView;
  141. beforeEach(function(){
  142. collectionView = new CollectionView({
  143. });
  144. spyOn(collectionView, "onRender").andCallThrough();
  145. spyOn(collectionView, "beforeRender").andCallThrough();
  146. spyOn(collectionView, "trigger").andCallThrough();
  147. collectionView.render();
  148. });
  149. it("should not append any html", function(){
  150. expect($(collectionView.$el)).not.toHaveHtml("<span>bar</span><span>baz</span>");
  151. });
  152. it("should not reference any view items", function(){
  153. expect(_.size(collectionView.children)).toBe(0);
  154. });
  155. });
  156. describe("emptyView", function(){
  157. var EmptyView = Backbone.Marionette.ItemView.extend({
  158. tagName: "span",
  159. className: "isempty",
  160. render: function(){}
  161. });
  162. var EmptyCollectionView = Backbone.Marionette.CollectionView.extend({
  163. itemView: ItemView,
  164. emptyView: EmptyView
  165. });
  166. describe("when rendering a collection view with an empty collection", function(){
  167. var collectionView;
  168. beforeEach(function(){
  169. var collection = new Collection();
  170. collectionView = new EmptyCollectionView({
  171. collection: collection
  172. });
  173. collectionView.render();
  174. });
  175. it("should append the html for the emptyView", function(){
  176. expect($(collectionView.$el)).toHaveHtml("<span class=\"isempty\"></span>");
  177. });
  178. it("should reference each of the rendered view items", function(){
  179. expect(_.size(collectionView.children)).toBe(1);
  180. });
  181. });
  182. describe("when the emptyView has been rendered for an empty collection, then adding an item to the collection", function(){
  183. var collectionView, closeSpy;
  184. beforeEach(function(){
  185. var collection = new Collection();
  186. collectionView = new EmptyCollectionView({
  187. collection: collection
  188. });
  189. collectionView.render();
  190. closeSpy = spyOn(EmptyView.prototype, "close");
  191. collection.add({foo: "wut"});
  192. });
  193. it("should close the emptyView", function(){
  194. expect(closeSpy).toHaveBeenCalled();
  195. });
  196. it("should show the new item", function(){
  197. expect(collectionView.$el).toHaveText(/wut/);
  198. });
  199. });
  200. describe("when the last item is removed from a collection", function(){
  201. var collectionView, closeSpy;
  202. beforeEach(function(){
  203. var collection = new Collection([{foo: "wut"}]);
  204. collectionView = new EmptyCollectionView({
  205. collection: collection
  206. });
  207. collectionView.render();
  208. collection.remove(collection.at(0));
  209. });
  210. it("should append the html for the emptyView", function(){
  211. expect($(collectionView.$el)).toHaveHtml("<span class=\"isempty\"></span>");
  212. });
  213. it("should reference each of the rendered view items", function(){
  214. expect(_.size(collectionView.children)).toBe(1);
  215. });
  216. });
  217. });
  218. describe("when a collection is reset after the view is loaded", function(){
  219. var collection;
  220. var collectionView;
  221. beforeEach(function(){
  222. collection = new Collection();
  223. collectionView = new CollectionView({
  224. collection: collection
  225. });
  226. spyOn(collectionView, "onRender").andCallThrough();
  227. spyOn(collectionView, "closeChildren").andCallThrough();
  228. collectionView.render();
  229. collection.reset([{foo: "bar"}, {foo: "baz"}]);
  230. });
  231. it("should close all open child views", function(){
  232. expect(collectionView.closeChildren).toHaveBeenCalled();
  233. });
  234. it("should append the html for each itemView", function(){
  235. expect($(collectionView.$el)).toHaveHtml("<span>bar</span><span>baz</span>");
  236. });
  237. it("should reference each of the rendered view items", function(){
  238. expect(_.size(collectionView.children)).toBe(2);
  239. });
  240. it("should call 'onRender' after rendering", function(){
  241. expect(collectionView.onRender).toHaveBeenCalled();
  242. });
  243. });
  244. describe("when a model is added to the collection", function(){
  245. var collectionView;
  246. var collection;
  247. var model;
  248. beforeEach(function(){
  249. spyOn(ItemView.prototype, "onRender");
  250. collection = new Collection();
  251. collectionView = new CollectionView({
  252. itemView: ItemView,
  253. collection: collection
  254. });
  255. collectionView.render();
  256. spyOn(collectionView, "appendHtml").andCallThrough();
  257. model = new Model({foo: "bar"});
  258. collection.add(model);
  259. });
  260. it("should add the model to the list", function(){
  261. expect(_.size(collectionView.children)).toBe(1);
  262. });
  263. it("should render the model in to the DOM", function(){
  264. expect($(collectionView.$el)).toHaveText("bar");
  265. });
  266. it("should provide the index for each itemView, when appending", function(){
  267. expect(collectionView.appendHtml.calls[0].args[2]).toBe(0);
  268. });
  269. });
  270. describe("when a model is removed from the collection", function(){
  271. var collectionView;
  272. var collection;
  273. var childView;
  274. var model;
  275. beforeEach(function(){
  276. model = new Model({foo: "bar"});
  277. collection = new Collection();
  278. collection.add(model);
  279. collectionView = new CollectionView({
  280. itemView: ItemView,
  281. collection: collection
  282. });
  283. collectionView.render();
  284. childView = collectionView.children[model.cid];
  285. spyOn(childView, "close").andCallThrough();
  286. collection.remove(model);
  287. });
  288. it("should close the model's view", function(){
  289. expect(childView.close).toHaveBeenCalled();
  290. });
  291. it("should remove the model-view's HTML", function(){
  292. expect($(collectionView.$el).children().length).toBe(0);
  293. });
  294. });
  295. describe("when closing a collection view", function(){
  296. var collectionView;
  297. var collection;
  298. var childView;
  299. var childModel;
  300. beforeEach(function(){
  301. collection = new Collection([{foo: "bar"}, {foo: "baz"}]);
  302. collectionView = new EventedView({
  303. template: "#itemTemplate",
  304. collection: collection
  305. });
  306. collectionView.someItemViewCallback = function(){};
  307. collectionView.render();
  308. childModel = collection.at(0);
  309. childView = collectionView.children[childModel.cid];
  310. collectionView.bindTo(collection, "foo", collectionView.someCallback);
  311. collectionView.bindTo(collectionView, "item:foo", collectionView.someItemViewCallback);
  312. spyOn(childView, "close").andCallThrough();
  313. spyOn(collectionView, "removeItemView").andCallThrough();
  314. spyOn(collectionView, "unbind").andCallThrough();
  315. spyOn(collectionView, "unbindAll").andCallThrough();
  316. spyOn(collectionView, "remove").andCallThrough();
  317. spyOn(collectionView, "someCallback").andCallThrough();
  318. spyOn(collectionView, "someItemViewCallback").andCallThrough();
  319. spyOn(collectionView, "close").andCallThrough();
  320. spyOn(collectionView, "onClose").andCallThrough();
  321. spyOn(collectionView, "beforeClose").andCallThrough();
  322. spyOn(collectionView, "trigger").andCallThrough();
  323. collectionView.close();
  324. childView.trigger("foo");
  325. collection.trigger("foo");
  326. collection.remove(childModel);
  327. });
  328. it("should close all of the child views", function(){
  329. expect(childView.close).toHaveBeenCalled();
  330. });
  331. it("should unbind all the bindTo events", function(){
  332. expect(collectionView.unbindAll).toHaveBeenCalled();
  333. });
  334. it("should unbind all collection events for the view", function(){
  335. expect(collectionView.someCallback).not.toHaveBeenCalled();
  336. });
  337. it("should unbind all item-view events for the view", function(){
  338. expect(collectionView.someItemViewCallback).not.toHaveBeenCalled();
  339. });
  340. it("should not retain any references to its children", function(){
  341. expect(_.size(collectionView.children)).toBe(0);
  342. });
  343. it("should not retain any bindings to its children", function(){
  344. expect(_.size(collectionView.bindings)).toBe(0);
  345. });
  346. it("should unbind any listener to custom view events", function(){
  347. expect(collectionView.unbind).toHaveBeenCalled();
  348. });
  349. it("should remove the view's EL from the DOM", function(){
  350. expect(collectionView.remove).toHaveBeenCalled();
  351. });
  352. it("should call `onClose` if provided", function(){
  353. expect(collectionView.onClose).toHaveBeenCalled();
  354. });
  355. it("should call `beforeClose` if provided", function(){
  356. expect(collectionView.beforeClose).toHaveBeenCalled();
  357. });
  358. it("should trigger a 'before:close' event", function(){
  359. expect(collectionView.trigger).toHaveBeenCalledWith("collection:before:close");
  360. });
  361. it("should trigger a 'closed", function(){
  362. expect(collectionView.trigger).toHaveBeenCalledWith("collection:closed");
  363. });
  364. });
  365. describe("when override appendHtml", function(){
  366. var collection = new Collection([{foo: "bar"}, {foo: "baz"}]);
  367. var collectionView;
  368. beforeEach(function(){
  369. collectionView = new PrependHtmlView({
  370. collection: collection
  371. });
  372. collectionView.render();
  373. });
  374. it("should append via the overridden method", function(){
  375. expect($(collectionView.$el)).toHaveHtml("<span>baz</span><span>bar</span>");
  376. });
  377. });
  378. describe("when a child view triggers an event", function(){
  379. var model = new Model({foo: "bar"});
  380. var collection = new Collection([model]);
  381. var collectionView;
  382. var childView;
  383. var triggeringView;
  384. var eventArgs;
  385. beforeEach(function(){
  386. collectionView = new PrependHtmlView({
  387. collection: collection
  388. });
  389. collectionView.render();
  390. collectionView.on("itemview:some:event", function(){
  391. eventArgs = Array.prototype.slice.call(arguments);
  392. });
  393. spyOn(collectionView, "trigger").andCallThrough();
  394. childView = collectionView.children[model.cid];
  395. childView.trigger("some:event", "test", model);
  396. });
  397. it("should bubble up through the parent collection view", function(){
  398. expect(collectionView.trigger).toHaveBeenCalledWith("itemview:some:event", childView, "test", model);
  399. });
  400. it("should provide the child view that triggered the event as the first parameter", function(){
  401. expect(eventArgs[0]).toBe(childView);
  402. });
  403. it("should forward all other arguments in order", function(){
  404. expect(eventArgs[1]).toBe("test");
  405. expect(eventArgs[2]).toBe(model);
  406. });
  407. });
  408. describe("when a child view is removed from a collection view", function(){
  409. var model;
  410. var collection;
  411. var collectionView;
  412. var childView;
  413. beforeEach(function(){
  414. model = new Model({foo: "bar"});
  415. collection = new Collection([model]);
  416. collectionView = new EventedView({
  417. template: "#itemTemplate",
  418. collection: collection
  419. });
  420. collectionView.render();
  421. childView = collectionView.children[model.cid];
  422. collection.remove(model)
  423. });
  424. it("should not retain any bindings to this view", function(){
  425. expect(_.any(collectionView.bindings, function(binding) {
  426. return binding.obj === childView;
  427. })).toBe(false);
  428. });
  429. it("should not retain any references to this view", function(){
  430. expect(_.size(collectionView.children)).toBe(0);
  431. });
  432. });
  433. describe("when the collection of a collection view is reset", function(){
  434. var model;
  435. var collection;
  436. var collectionView;
  437. var childView;
  438. beforeEach(function(){
  439. model = new Model({foo: "bar"});
  440. collection = new Collection([model]);
  441. collectionView = new EventedView({
  442. template: "#itemTemplate",
  443. collection: collection
  444. });
  445. collectionView.render();
  446. childView = collectionView.children[model.cid];
  447. collection.reset();
  448. });
  449. it("should not retain any references to the previous views", function(){
  450. expect(_.size(collectionView.children)).toBe(0);
  451. });
  452. it("should not retain any bindings to the previous views", function(){
  453. expect(_.any(collectionView.bindings, function(binding) {
  454. return binding.obj === childView;
  455. })).toBe(false);
  456. });
  457. });
  458. describe("when a child view is added to a collection view, after the collection view has been shown", function(){
  459. var m1, m2, col, view, viewOnShowContext;
  460. var ItemView = Backbone.Marionette.ItemView.extend({
  461. onShow: function(){ viewOnShowContext = this; },
  462. onRender: function(){},
  463. render: function(){}
  464. });
  465. var ColView = Backbone.Marionette.CollectionView.extend({
  466. itemView: ItemView,
  467. onShow: function(){}
  468. });
  469. beforeEach(function(){
  470. spyOn(ItemView.prototype, "onShow").andCallThrough();
  471. m1 = new Model();
  472. m2 = new Model();
  473. col = new Collection([m1]);
  474. var colView = new ColView({
  475. collection: col
  476. });
  477. colView.render();
  478. colView.onShow();
  479. colView.trigger("show");
  480. col.add(m2);
  481. view = colView.children[m2.cid];
  482. });
  483. it("should call the 'onShow' method of the child view", function(){
  484. expect(ItemView.prototype.onShow).toHaveBeenCalled();
  485. });
  486. it("should call the child's 'onShow' method with itself as the context", function(){
  487. expect(viewOnShowContext).toBe(view);
  488. });
  489. });
  490. });