PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

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