PageRenderTime 54ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

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