PageRenderTime 36ms CodeModel.GetById 2ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

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