PageRenderTime 63ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

https://github.com/serkanserttop/backbone.marionette
JavaScript | 548 lines | 400 code | 135 blank | 13 comment | 1 complexity | 1711c906c1f10abedba94c60c586f7c9 MD5 | raw file
  1. describe("composite view", function(){
  2. describe("when a composite view has a template without a model", function(){
  3. var compositeView;
  4. beforeEach(function(){
  5. loadFixtures("compositeTemplate-noModel.html");
  6. var m1 = new Model({foo: "bar"});
  7. var m2 = new Model({foo: "baz"});
  8. collection = new Collection([m1, m2]);
  9. compositeView = new CompositeViewNoModel({
  10. collection: collection
  11. });
  12. compositeView.render();
  13. });
  14. it("should render the template", function(){
  15. expect(compositeView.$el).toHaveText(/composite/);
  16. });
  17. it("should render the collection's items", function(){
  18. expect(compositeView.$el).toHaveText(/bar/);
  19. expect(compositeView.$el).toHaveText(/baz/);
  20. });
  21. });
  22. describe("when a composite view has a model and a template", function(){
  23. var compositeView;
  24. beforeEach(function(){
  25. loadFixtures("compositeTemplate.html");
  26. var m1 = new Model({foo: "bar"});
  27. var m2 = new Model({foo: "baz"});
  28. collection = new Collection();
  29. collection.add(m2);
  30. compositeView = new CompositeView({
  31. model: m1,
  32. collection: collection
  33. });
  34. compositeView.render();
  35. });
  36. it("should render the template with the model", function(){
  37. expect(compositeView.$el).toHaveText(/composite bar/);
  38. });
  39. it("should render the collection's items", function(){
  40. expect(compositeView.$el).toHaveText(/baz/);
  41. });
  42. });
  43. describe("when rendering a composite view", function(){
  44. var compositeView;
  45. var order;
  46. var deferredResolved;
  47. beforeEach(function(){
  48. order = [];
  49. loadFixtures("compositeTemplate.html");
  50. var m1 = new Model({foo: "bar"});
  51. var m2 = new Model({foo: "baz"});
  52. var collection = new Collection();
  53. collection.add(m2);
  54. compositeView = new CompositeView({
  55. model: m1,
  56. collection: collection
  57. });
  58. compositeView.on("composite:model:rendered", function(){
  59. order.push(compositeView.renderedModelView);
  60. });
  61. compositeView.on("composite:collection:rendered", function(){
  62. order.push(compositeView.collection);
  63. });
  64. compositeView.on("composite:rendered", function(){
  65. order.push(compositeView);
  66. });
  67. spyOn(compositeView, "trigger").andCallThrough();
  68. spyOn(compositeView, "onRender").andCallThrough();
  69. compositeView.render();
  70. });
  71. it("should trigger a rendered event for the model view", function(){
  72. expect(compositeView.trigger).toHaveBeenCalledWith("composite:model:rendered");
  73. });
  74. it("should trigger a rendered event for the collection", function(){
  75. expect(compositeView.trigger).toHaveBeenCalledWith("composite:collection:rendered");
  76. });
  77. it("should trigger a rendered event for the composite view", function(){
  78. expect(compositeView.trigger).toHaveBeenCalledWith("composite:rendered");
  79. });
  80. it("should guarantee rendering of the model before rendering the collection", function(){
  81. expect(order[0]).toBe(compositeView.renderedModelView);
  82. expect(order[1]).toBe(compositeView.collection);
  83. expect(order[2]).toBe(compositeView);
  84. });
  85. it("should call 'onRender'", function(){
  86. expect(compositeView.onRender).toHaveBeenCalled();
  87. });
  88. });
  89. describe("when rendering a composite view twice", function(){
  90. var compositeView, compositeRenderSpy;
  91. beforeEach(function(){
  92. loadFixtures("compositeTemplate.html");
  93. var m1 = new Model({foo: "bar"});
  94. var m2 = new Model({foo: "baz"});
  95. collection = new Collection();
  96. collection.add(m2);
  97. compositeView = new CompositeModelView({
  98. model: m1,
  99. collection: collection
  100. });
  101. spyOn(compositeView, "render").andCallThrough();
  102. spyOn(compositeView, "closeChildren").andCallThrough();
  103. spyOn(Backbone.Marionette.Renderer, "render");
  104. compositeRenderSpy = compositeView.render;
  105. compositeView.render();
  106. compositeView.render();
  107. });
  108. it("should re-render the template view", function(){
  109. expect(Backbone.Marionette.Renderer.render.callCount).toBe(2);
  110. });
  111. it("should close all of the child collection item views", function(){
  112. expect(compositeView.closeChildren).toHaveBeenCalled();
  113. expect(compositeView.closeChildren.callCount).toBe(2);
  114. });
  115. it("should re-render the collection's items", function(){
  116. expect(compositeRenderSpy.callCount).toBe(2);
  117. });
  118. });
  119. describe("when rendering a composite view with an empty collection and then resetting the collection", function(){
  120. var compositeView;
  121. beforeEach(function(){
  122. loadFixtures("compositeRerender.html");
  123. var m1 = new Model({foo: "bar"});
  124. var collection = new Collection();
  125. compositeView = new CompositeView({
  126. model: m1,
  127. collection: collection
  128. });
  129. compositeView.render();
  130. var m2 = new Model({foo: "baz"});
  131. collection.reset([m2]);
  132. });
  133. it("should render the template with the model", function(){
  134. expect(compositeView.$el).toHaveText(/composite bar/);
  135. });
  136. it("should render the collection's items", function(){
  137. expect(compositeView.$el).toHaveText(/baz/);
  138. });
  139. });
  140. describe("when rendering a composite view without a collection", function(){
  141. var compositeView;
  142. beforeEach(function(){
  143. loadFixtures("compositeRerender.html");
  144. var m1 = new Model({foo: "bar"});
  145. compositeView = new CompositeView({
  146. model: m1
  147. });
  148. compositeView.render();
  149. });
  150. it("should render the template with the model", function(){
  151. expect(compositeView.$el).toHaveText(/composite bar/);
  152. });
  153. it("should not render the collection's items", function(){
  154. expect(compositeView.$el).not.toHaveText(/baz/);
  155. });
  156. });
  157. describe("when rendering a composite with a collection and then resetting the collection", function(){
  158. var compositeView;
  159. beforeEach(function(){
  160. loadFixtures("compositeRerender.html");
  161. var m1 = new Model({foo: "bar"});
  162. var m2 = new Model({foo: "baz"});
  163. var collection = new Collection([m2]);
  164. compositeView = new CompositeView({
  165. model: m1,
  166. collection: collection
  167. });
  168. compositeView.render();
  169. spyOn(compositeView, "renderModel").andCallThrough();
  170. var m3 = new Model({foo: "quux"});
  171. var m4 = new Model({foo: "widget"});
  172. collection.reset([m3, m4]);
  173. });
  174. it("should not re-render the template with the model", function(){
  175. expect(compositeView.renderModel).not.toHaveBeenCalled();
  176. });
  177. it("should render the collection's items", function(){
  178. expect(compositeView.$el).not.toHaveText(/baz/);
  179. expect(compositeView.$el).toHaveText(/quux/);
  180. expect(compositeView.$el).toHaveText(/widget/);
  181. });
  182. });
  183. describe("when workign with a composite and recursive model", function(){
  184. var treeView;
  185. beforeEach(function(){
  186. loadFixtures("recursiveCompositeTemplate.html");
  187. var data = {
  188. name: "level 1",
  189. nodes: [
  190. {
  191. name: "level 2",
  192. nodes: [
  193. {
  194. name: "level 3"
  195. }
  196. ]
  197. }
  198. ]
  199. };
  200. var node = new Node(data);
  201. treeView = new TreeView({
  202. model: node
  203. });
  204. treeView.render();
  205. });
  206. it("should render the template with the model", function(){
  207. expect(treeView.$el).toHaveText(/level 1/);
  208. });
  209. it("should render the collection's items", function(){
  210. expect(treeView.$el).toHaveText(/level 2/);
  211. });
  212. it("should render all the levels of the nested object", function(){
  213. expect(treeView.$el).toHaveText(/level 3/);
  214. });
  215. });
  216. describe("when closing a composite view", function(){
  217. var compositeView, compositeModelCloseSpy;
  218. beforeEach(function(){
  219. loadFixtures("compositeTemplate.html");
  220. var m1 = new Model({foo: "bar"});
  221. var m2 = new Model({foo: "baz"});
  222. collection = new Collection();
  223. collection.add(m2);
  224. compositeView = new CompositeModelView({
  225. model: m1,
  226. collection: collection
  227. });
  228. spyOn(CompositeModelView.prototype, "close").andCallThrough();
  229. compositeView.render();
  230. compositeView.close();
  231. });
  232. it("should delete the model view", function(){
  233. expect(compositeView.renderedModelView).toBeUndefined();
  234. });
  235. it("should close the collection of views", function(){
  236. expect(CompositeModelView.prototype.close.callCount).toBe(1);
  237. });
  238. });
  239. describe("when rendering a composite view with no model, using a template to create a grid", function(){
  240. var gridView;
  241. beforeEach(function(){
  242. loadFixtures("gridTemplates.html");
  243. var userData = [
  244. {
  245. username: "dbailey",
  246. fullname: "Derick Bailey"
  247. },
  248. {
  249. username: "jbob",
  250. fullname: "Joe Bob"
  251. },
  252. {
  253. username: "fbar",
  254. fullname: "Foo Bar"
  255. }
  256. ];
  257. var userList = new UserCollection(userData);
  258. gridView = new GridView({
  259. tagName: "table",
  260. collection: userList
  261. });
  262. gridView.render();
  263. });
  264. it("should render the table", function(){
  265. expect(gridView.$("th").length).not.toBe(0);
  266. });
  267. it("should render the users", function(){
  268. var body = gridView.$("tbody");
  269. expect(body).toHaveText(/dbailey/);
  270. expect(body).toHaveText(/jbob/);
  271. expect(body).toHaveText(/fbar/);
  272. });
  273. });
  274. describe("when a composite view has a ui elements hash", function() {
  275. var gridView, headersModel;
  276. beforeEach(function() {
  277. loadFixtures("uiBindingTemplate.html");
  278. var userData = [
  279. {
  280. username: "dbailey",
  281. fullname: "Derick Bailey"
  282. },
  283. {
  284. username: "jbob",
  285. fullname: "Joe Bob"
  286. }
  287. ];
  288. headersModel = new Backbone.Model({
  289. userHeader: "Username",
  290. nameHeader: "Full name"
  291. });
  292. var userList = new UserCollection(userData);
  293. gridView = new GridViewWithUIBindings({
  294. tagName: "table",
  295. model: headersModel,
  296. collection: userList
  297. });
  298. // We don't render the view here since we need more fine-tuned control on when the view is rendered,
  299. // specifically in the test that asserts the composite view template elements are accessible before
  300. // the collection is rendered.
  301. });
  302. describe("after the whole composite view finished rendering", function() {
  303. beforeEach(function() {
  304. gridView.render();
  305. });
  306. describe("accessing a ui element that belongs to the model template", function() {
  307. it("should return its jQuery selector if it can be found", function() {
  308. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
  309. });
  310. it("should return an empty jQuery object if it cannot be found", function() {
  311. expect(gridView.ui.unfoundElement.length).toEqual(0);
  312. });
  313. it("should return an up-to-date selector on subsequent renders", function() {
  314. // asserting state before subsequent render
  315. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
  316. headersModel.set("userHeader", "User");
  317. gridView.render();
  318. expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("User");
  319. });
  320. });
  321. describe("accessing a ui element that belongs to the collection", function() {
  322. // This test makes it clear that not allowing access to the collection elements is a design decision
  323. // and not a bug.
  324. it("should return an empty jQuery object", function() {
  325. expect(gridView.ui.itemRows.length).toEqual(0);
  326. });
  327. });
  328. });
  329. describe("after the model finished rendering, but before the collection rendered", function() {
  330. describe("accessing a ui element that belongs to the model template", function() {
  331. // this test enforces that ui elements should be accessible as soon as their html was inserted
  332. // to the DOM
  333. it("should return its jQuery selector", function() {
  334. gridView.beforeRender = function() {
  335. expect(gridView.ui.headersRow.find("th:first-child").text()).toEqual("Username");
  336. };
  337. gridView.render();
  338. })
  339. });
  340. });
  341. });
  342. // Models
  343. var Model = Backbone.Model.extend({});
  344. var User = Backbone.Model.extend({});
  345. var Node = Backbone.Model.extend({
  346. initialize: function(){
  347. var nodes = this.get("nodes");
  348. if (nodes){
  349. this.nodes = new NodeCollection(nodes);
  350. this.unset("nodes");
  351. }
  352. }
  353. });
  354. // Collections
  355. var Collection = Backbone.Collection.extend({
  356. model: Model
  357. });
  358. var UserCollection = Backbone.Collection.extend({
  359. model: User
  360. });
  361. var NodeCollection = Backbone.Collection.extend({
  362. model: Node
  363. });
  364. // Views
  365. var ItemView = Backbone.Marionette.ItemView.extend({
  366. tagName: "span",
  367. render: function(){
  368. this.$el.html(this.model.get("foo"));
  369. }
  370. });
  371. var TreeView = Backbone.Marionette.CompositeView.extend({
  372. tagName: "ul",
  373. template: "#recursive-composite-template",
  374. initialize: function(){
  375. this.collection = this.model.nodes;
  376. }
  377. });
  378. // A Grid Row
  379. var GridRow = Backbone.Marionette.ItemView.extend({
  380. tagName: "tr",
  381. template: "#row-template"
  382. });
  383. // The grid view
  384. var GridView = Backbone.Marionette.CompositeView.extend({
  385. tagName: "table",
  386. template: "#grid-template",
  387. itemView: GridRow,
  388. appendHtml: function(cv, iv){
  389. cv.$("tbody").append(iv.el);
  390. }
  391. });
  392. GridViewWithUIBindings = GridView.extend({
  393. template: "#ui-binding-template",
  394. ui: {
  395. headersRow: "thead tr",
  396. unfoundElement: "#unfound",
  397. itemRows: "tbody tr"
  398. }
  399. });
  400. var CompositeView = Backbone.Marionette.CompositeView.extend({
  401. itemView: ItemView,
  402. template: "#composite-template",
  403. onRender: function(){}
  404. });
  405. var CompositeViewNoModel = Backbone.Marionette.CompositeView.extend({
  406. itemView: ItemView,
  407. template: "#composite-template-no-model"
  408. });
  409. var CompositeModelView = Backbone.Marionette.CompositeView.extend({
  410. itemView: ItemView,
  411. template: "#composite-template"
  412. });
  413. });