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