PageRenderTime 87ms CodeModel.GetById 32ms RepoModel.GetById 7ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

https://github.com/vespakoen/backbone.marionette
JavaScript | 707 lines | 523 code | 169 blank | 15 comment | 2 complexity | 8b081ada92fe47b2c6277f7e8de5a456 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", function(){
  104. var compositeView, order, 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. template: "#composite-template",
  114. onRender: function(){}
  115. });
  116. beforeEach(function(){
  117. order = [];
  118. loadFixtures("compositeTemplate.html");
  119. var m1 = new Model({foo: "bar"});
  120. var m2 = new Model({foo: "baz"});
  121. var collection = new Collection();
  122. collection.add(m2);
  123. compositeView = new CompositeView({
  124. model: m1,
  125. collection: collection
  126. });
  127. compositeView.on("composite:model:rendered", function(){
  128. order.push(compositeView.renderedModelView);
  129. });
  130. compositeView.on("composite:collection:rendered", function(){
  131. order.push(compositeView.collection);
  132. });
  133. compositeView.on("composite:rendered", function(){
  134. order.push(compositeView);
  135. });
  136. spyOn(compositeView, "trigger").andCallThrough();
  137. spyOn(compositeView, "onRender").andCallThrough();
  138. compositeView.render();
  139. });
  140. it("should trigger a rendered event for the model view", function(){
  141. expect(compositeView.trigger).toHaveBeenCalledWith("composite:model:rendered");
  142. });
  143. it("should trigger a rendered event for the collection", function(){
  144. expect(compositeView.trigger).toHaveBeenCalledWith("composite:collection:rendered");
  145. });
  146. it("should trigger a rendered event for the composite view", function(){
  147. expect(compositeView.trigger).toHaveBeenCalledWith("composite:rendered");
  148. });
  149. it("should guarantee rendering of the model before rendering the collection", function(){
  150. expect(order[0]).toBe(compositeView.renderedModelView);
  151. expect(order[1]).toBe(compositeView.collection);
  152. expect(order[2]).toBe(compositeView);
  153. });
  154. it("should call 'onRender'", function(){
  155. expect(compositeView.onRender).toHaveBeenCalled();
  156. });
  157. it("should only call 'onRender' once", function(){
  158. expect(compositeView.onRender.callCount).toBe(1);
  159. })
  160. });
  161. describe("when rendering a composite view twice", function(){
  162. var compositeView, compositeRenderSpy;
  163. var ItemView = Backbone.Marionette.ItemView.extend({
  164. tagName: "span",
  165. render: function(){
  166. this.$el.html(this.model.get("foo"));
  167. }
  168. });
  169. var CompositeModelView = Backbone.Marionette.CompositeView.extend({
  170. itemView: ItemView,
  171. template: "#composite-template"
  172. });
  173. beforeEach(function(){
  174. loadFixtures("compositeTemplate.html");
  175. var m1 = new Model({foo: "bar"});
  176. var m2 = new Model({foo: "baz"});
  177. var collection = new Collection();
  178. collection.add(m2);
  179. compositeView = new CompositeModelView({
  180. model: m1,
  181. collection: collection
  182. });
  183. spyOn(compositeView, "render").andCallThrough();
  184. spyOn(compositeView, "closeChildren").andCallThrough();
  185. spyOn(Backbone.Marionette.Renderer, "render");
  186. compositeRenderSpy = compositeView.render;
  187. compositeView.render();
  188. compositeView.render();
  189. });
  190. it("should re-render the template view", function(){
  191. expect(Backbone.Marionette.Renderer.render.callCount).toBe(2);
  192. });
  193. it("should close all of the child collection item views", function(){
  194. expect(compositeView.closeChildren).toHaveBeenCalled();
  195. expect(compositeView.closeChildren.callCount).toBe(2);
  196. });
  197. it("should re-render the collection's items", function(){
  198. expect(compositeRenderSpy.callCount).toBe(2);
  199. });
  200. });
  201. describe("when rendering a composite view with an empty collection and then resetting the collection", function(){
  202. var compositeView;
  203. var ItemView = Backbone.Marionette.ItemView.extend({
  204. tagName: "span",
  205. render: function(){
  206. this.$el.html(this.model.get("foo"));
  207. }
  208. });
  209. var CompositeView = Backbone.Marionette.CompositeView.extend({
  210. itemView: ItemView,
  211. template: "#composite-template",
  212. onRender: function(){}
  213. });
  214. beforeEach(function(){
  215. loadFixtures("compositeRerender.html");
  216. var m1 = new Model({foo: "bar"});
  217. var collection = new Collection();
  218. compositeView = new CompositeView({
  219. model: m1,
  220. collection: collection
  221. });
  222. compositeView.render();
  223. var m2 = new Model({foo: "baz"});
  224. collection.reset([m2]);
  225. });
  226. it("should render the template with the model", function(){
  227. expect(compositeView.$el).toHaveText(/composite bar/);
  228. });
  229. it("should render the collection's items", function(){
  230. expect(compositeView.$el).toHaveText(/baz/);
  231. });
  232. });
  233. describe("when rendering a composite view without a collection", function(){
  234. var compositeView;
  235. var ItemView = Backbone.Marionette.ItemView.extend({
  236. tagName: "span",
  237. render: function(){
  238. this.$el.html(this.model.get("foo"));
  239. }
  240. });
  241. var CompositeView = Backbone.Marionette.CompositeView.extend({
  242. itemView: ItemView,
  243. template: "#composite-template",
  244. onRender: function(){}
  245. });
  246. beforeEach(function(){
  247. loadFixtures("compositeRerender.html");
  248. var m1 = new Model({foo: "bar"});
  249. compositeView = new CompositeView({
  250. model: m1
  251. });
  252. compositeView.render();
  253. });
  254. it("should render the template with the model", function(){
  255. expect(compositeView.$el).toHaveText(/composite bar/);
  256. });
  257. it("should not render the collection's items", function(){
  258. expect(compositeView.$el).not.toHaveText(/baz/);
  259. });
  260. });
  261. describe("when rendering a composite with a collection and then resetting the collection", function(){
  262. var compositeView;
  263. var ItemView = Backbone.Marionette.ItemView.extend({
  264. tagName: "span",
  265. render: function(){
  266. this.$el.html(this.model.get("foo"));
  267. }
  268. });
  269. var CompositeView = Backbone.Marionette.CompositeView.extend({
  270. itemView: ItemView,
  271. template: "#composite-template",
  272. onRender: function(){}
  273. });
  274. beforeEach(function(){
  275. loadFixtures("compositeRerender.html");
  276. var m1 = new Model({foo: "bar"});
  277. var m2 = new Model({foo: "baz"});
  278. var collection = new Collection([m2]);
  279. compositeView = new CompositeView({
  280. model: m1,
  281. collection: collection
  282. });
  283. compositeView.render();
  284. spyOn(compositeView, "renderModel").andCallThrough();
  285. var m3 = new Model({foo: "quux"});
  286. var m4 = new Model({foo: "widget"});
  287. collection.reset([m3, m4]);
  288. });
  289. it("should not re-render the template with the model", function(){
  290. expect(compositeView.renderModel).not.toHaveBeenCalled();
  291. });
  292. it("should render the collection's items", function(){
  293. expect(compositeView.$el).not.toHaveText(/baz/);
  294. expect(compositeView.$el).toHaveText(/quux/);
  295. expect(compositeView.$el).toHaveText(/widget/);
  296. });
  297. });
  298. describe("when workign with a composite and recursive model", function(){
  299. var treeView;
  300. beforeEach(function(){
  301. loadFixtures("recursiveCompositeTemplate.html");
  302. var data = {
  303. name: "level 1",
  304. nodes: [
  305. {
  306. name: "level 2",
  307. nodes: [
  308. {
  309. name: "level 3"
  310. }
  311. ]
  312. }
  313. ]
  314. };
  315. var node = new Node(data);
  316. treeView = new TreeView({
  317. model: node
  318. });
  319. treeView.render();
  320. });
  321. it("should render the template with the model", function(){
  322. expect(treeView.$el).toHaveText(/level 1/);
  323. });
  324. it("should render the collection's items", function(){
  325. expect(treeView.$el).toHaveText(/level 2/);
  326. });
  327. it("should render all the levels of the nested object", function(){
  328. expect(treeView.$el).toHaveText(/level 3/);
  329. });
  330. });
  331. describe("when closing a composite view", function(){
  332. var compositeView, compositeModelCloseSpy;
  333. var ItemView = Backbone.Marionette.ItemView.extend({
  334. tagName: "span",
  335. render: function(){
  336. this.$el.html(this.model.get("foo"));
  337. }
  338. });
  339. var CompositeModelView = Backbone.Marionette.CompositeView.extend({
  340. itemView: ItemView,
  341. template: "#composite-template"
  342. });
  343. beforeEach(function(){
  344. loadFixtures("compositeTemplate.html");
  345. var m1 = new Model({foo: "bar"});
  346. var m2 = new Model({foo: "baz"});
  347. var collection = new Collection();
  348. collection.add(m2);
  349. compositeView = new CompositeModelView({
  350. model: m1,
  351. collection: collection
  352. });
  353. spyOn(CompositeModelView.prototype, "close").andCallThrough();
  354. compositeView.render();
  355. compositeView.close();
  356. });
  357. it("should delete the model view", function(){
  358. expect(compositeView.renderedModelView).toBeUndefined();
  359. });
  360. it("should close the collection of views", function(){
  361. expect(CompositeModelView.prototype.close.callCount).toBe(1);
  362. });
  363. });
  364. describe("when rendering a composite view with no model, using a template to create a grid", function(){
  365. var gridView;
  366. // A Grid Row
  367. var GridRow = Backbone.Marionette.ItemView.extend({
  368. tagName: "tr",
  369. template: "#row-template"
  370. });
  371. // The grid view
  372. var GridView = Backbone.Marionette.CompositeView.extend({
  373. tagName: "table",
  374. template: "#grid-template",
  375. itemView: GridRow,
  376. appendHtml: function(cv, iv){
  377. cv.$("tbody").append(iv.el);
  378. }
  379. });
  380. beforeEach(function(){
  381. loadFixtures("gridTemplates.html");
  382. var userData = [
  383. {
  384. username: "dbailey",
  385. fullname: "Derick Bailey"
  386. },
  387. {
  388. username: "jbob",
  389. fullname: "Joe Bob"
  390. },
  391. {
  392. username: "fbar",
  393. fullname: "Foo Bar"
  394. }
  395. ];
  396. var userList = new UserCollection(userData);
  397. gridView = new GridView({
  398. tagName: "table",
  399. collection: userList
  400. });
  401. gridView.render();
  402. });
  403. it("should render the table", function(){
  404. expect(gridView.$("th").length).not.toBe(0);
  405. });
  406. it("should render the users", function(){
  407. var body = gridView.$("tbody");
  408. expect(body).toHaveText(/dbailey/);
  409. expect(body).toHaveText(/jbob/);
  410. expect(body).toHaveText(/fbar/);
  411. });
  412. });
  413. describe("when a composite view has a ui elements hash", function() {
  414. var called, gridView, headersModel;
  415. // A Grid Row
  416. var GridRow = Backbone.Marionette.ItemView.extend({
  417. tagName: "tr",
  418. template: "#row-template"
  419. });
  420. // The grid view
  421. var GridView = Backbone.Marionette.CompositeView.extend({
  422. tagName: "table",
  423. template: "#grid-template",
  424. itemView: GridRow,
  425. appendHtml: function(cv, iv){
  426. cv.$("tbody").append(iv.el);
  427. }
  428. });
  429. var GridViewWithUIBindings = GridView.extend({
  430. template: "#ui-binding-template",
  431. ui: {
  432. headersRow: "thead tr",
  433. unfoundElement: "#unfound",
  434. itemRows: "tbody tr"
  435. }
  436. });
  437. beforeEach(function() {
  438. loadFixtures("uiBindingTemplate.html");
  439. var userData = [
  440. {
  441. username: "dbailey",
  442. fullname: "Derick Bailey"
  443. },
  444. {
  445. username: "jbob",
  446. fullname: "Joe Bob"
  447. }
  448. ];
  449. headersModel = new Backbone.Model({
  450. userHeader: "Username",
  451. nameHeader: "Full name"
  452. });
  453. var userList = new UserCollection(userData);
  454. gridView = new GridViewWithUIBindings({
  455. tagName: "table",
  456. model: headersModel,
  457. collection: userList
  458. });
  459. // We don't render the view here since we need more fine-tuned control on when the view is rendered,
  460. // specifically in the test that asserts the composite view template elements are accessible before
  461. // the collection is rendered.
  462. });
  463. describe("after the whole composite view finished rendering", function() {
  464. beforeEach(function() {
  465. gridView.render();
  466. });
  467. describe("accessing a ui element that belongs to the model template", function() {
  468. it("should return its jQuery selector if it can be found", function() {
  469. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
  470. });
  471. it("should return an empty jQuery object if it cannot be found", function() {
  472. expect(gridView.ui.unfoundElement.length).toEqual(0);
  473. });
  474. it("should return an up-to-date selector on subsequent renders", function() {
  475. // asserting state before subsequent render
  476. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
  477. headersModel.set("userHeader", "User");
  478. gridView.render();
  479. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("User");
  480. });
  481. });
  482. describe("accessing a ui element that belongs to the collection", function() {
  483. // This test makes it clear that not allowing access to the collection elements is a design decision
  484. // and not a bug.
  485. it("should return an empty jQuery object", function() {
  486. expect(gridView.ui.itemRows.length).toEqual(0);
  487. });
  488. });
  489. });
  490. describe("after the model finished rendering, but before the collection rendered", function() {
  491. describe("accessing a ui element that belongs to the model template", function() {
  492. beforeEach(function(){
  493. gridView.onBeforeRender = function() {
  494. expect(gridView.ui.headersRow.find("th:first-child").text()).toEqual("Username");
  495. called = true;
  496. };
  497. spyOn(gridView, "onBeforeRender").andCallThrough();
  498. gridView.render();
  499. });
  500. // this test enforces that ui elements should be accessible as soon as their html was inserted
  501. // to the DOM
  502. it("should return its jQuery selector", function() {
  503. expect(gridView.onBeforeRender).toHaveBeenCalled();
  504. })
  505. });
  506. });
  507. });
  508. // Models
  509. var Model = Backbone.Model.extend({});
  510. var User = Backbone.Model.extend({});
  511. var Node = Backbone.Model.extend({
  512. initialize: function(){
  513. var nodes = this.get("nodes");
  514. if (nodes){
  515. this.nodes = new NodeCollection(nodes);
  516. this.unset("nodes");
  517. }
  518. }
  519. });
  520. // Collections
  521. var Collection = Backbone.Collection.extend({
  522. model: Model
  523. });
  524. var UserCollection = Backbone.Collection.extend({
  525. model: User
  526. });
  527. var NodeCollection = Backbone.Collection.extend({
  528. model: Node
  529. });
  530. // Views
  531. var TreeView = Backbone.Marionette.CompositeView.extend({
  532. tagName: "ul",
  533. template: "#recursive-composite-template",
  534. initialize: function(){
  535. this.collection = this.model.nodes;
  536. }
  537. });
  538. });