PageRenderTime 33ms CodeModel.GetById 2ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/compositeView.spec.js

https://github.com/patseng/backbone.marionette
JavaScript | 755 lines | 560 code | 180 blank | 15 comment | 2 complexity | b84f66b2a26ee057747f563e5acf8b0a 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 without a template", function(){
135    var compositeView, 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    });
147
148    beforeEach(function(){
149      var m1 = new Model({foo: "bar"});
150      var m2 = new Model({foo: "baz"});
151      var collection = new Collection();
152      collection.add(m2);
153
154      compositeView = new CompositeView({
155        model: m1,
156        collection: collection
157      });
158    });
159
160    it("should throw an exception because there was no valid template", function(){
161      expect(compositeView.render).toThrow(new Error("Cannot render the template since it's false, null or undefined."));
162    });
163  });
164  
165  describe("when rendering a composite view", function(){
166    var compositeView, order, deferredResolved;
167
168    var ItemView = Backbone.Marionette.ItemView.extend({
169      tagName: "span",
170      render: function(){
171        this.$el.html(this.model.get("foo"));
172      }
173    });
174
175    var CompositeView = Backbone.Marionette.CompositeView.extend({
176      itemView: ItemView,
177      template: "#composite-template",
178
179      onRender: function(){}
180    });
181
182    beforeEach(function(){
183      order = [];
184      loadFixtures("compositeTemplate.html");
185
186      var m1 = new Model({foo: "bar"});
187      var m2 = new Model({foo: "baz"});
188      var collection = new Collection();
189      collection.add(m2);
190
191      compositeView = new CompositeView({
192        model: m1,
193        collection: collection
194      });
195
196      compositeView.on("composite:model:rendered", function(){
197        order.push(compositeView.renderedModelView);
198      });
199
200      compositeView.on("composite:collection:rendered", function(){
201        order.push(compositeView.collection);
202      });
203
204      compositeView.on("composite:rendered", function(){
205        order.push(compositeView);
206      });
207
208      spyOn(compositeView, "trigger").andCallThrough();
209      spyOn(compositeView, "onRender").andCallThrough();
210
211      compositeView.render();
212    });
213
214    it("should trigger a rendered event for the model view", function(){
215      expect(compositeView.trigger).toHaveBeenCalledWith("composite:model:rendered");
216    });
217
218    it("should trigger a rendered event for the collection", function(){
219      expect(compositeView.trigger).toHaveBeenCalledWith("composite:collection:rendered");
220    });
221
222    it("should trigger a rendered event for the composite view", function(){
223      expect(compositeView.trigger).toHaveBeenCalledWith("composite:rendered");
224    });
225
226    it("should guarantee rendering of the model before rendering the collection", function(){
227      expect(order[0]).toBe(compositeView.renderedModelView);
228      expect(order[1]).toBe(compositeView.collection);
229      expect(order[2]).toBe(compositeView);
230    });
231
232    it("should call 'onRender'", function(){
233      expect(compositeView.onRender).toHaveBeenCalled();
234    });
235
236    it("should only call 'onRender' once", function(){
237      expect(compositeView.onRender.callCount).toBe(1);
238    })
239  });
240
241  describe("when rendering a composite view twice", function(){
242    var compositeView, compositeRenderSpy;
243
244    var ItemView = Backbone.Marionette.ItemView.extend({
245      tagName: "span",
246      render: function(){
247        this.$el.html(this.model.get("foo"));
248      }
249    });
250
251    var CompositeModelView = Backbone.Marionette.CompositeView.extend({
252      itemView: ItemView,
253      template: "#composite-template"
254    });
255
256    beforeEach(function(){
257      loadFixtures("compositeTemplate.html");
258
259      var m1 = new Model({foo: "bar"});
260      var m2 = new Model({foo: "baz"});
261      var collection = new Collection();
262      collection.add(m2);
263
264      compositeView = new CompositeModelView({
265        model: m1,
266        collection: collection
267      });
268
269      spyOn(compositeView, "render").andCallThrough();
270      spyOn(compositeView, "closeChildren").andCallThrough();
271      spyOn(Backbone.Marionette.Renderer, "render");
272      compositeRenderSpy = compositeView.render;
273
274      compositeView.render();
275      compositeView.render();
276    });
277
278    it("should re-render the template view", function(){
279      expect(Backbone.Marionette.Renderer.render.callCount).toBe(2);
280    });
281
282    it("should close all of the child collection item views", function(){
283      expect(compositeView.closeChildren).toHaveBeenCalled();
284      expect(compositeView.closeChildren.callCount).toBe(2);
285    });
286
287    it("should re-render the collection's items", function(){
288      expect(compositeRenderSpy.callCount).toBe(2);
289    });
290  });
291
292  describe("when rendering a composite view with an empty collection and then resetting the collection", function(){
293    var compositeView;
294
295    var ItemView = Backbone.Marionette.ItemView.extend({
296      tagName: "span",
297      render: function(){
298        this.$el.html(this.model.get("foo"));
299      }
300    });
301
302    var CompositeView = Backbone.Marionette.CompositeView.extend({
303      itemView: ItemView,
304      template: "#composite-template",
305
306      onRender: function(){}
307    });
308
309    beforeEach(function(){
310      loadFixtures("compositeRerender.html");
311
312      var m1 = new Model({foo: "bar"});
313      var collection = new Collection();
314      compositeView = new CompositeView({
315        model: m1,
316        collection: collection
317      });
318
319      compositeView.render();
320
321      var m2 = new Model({foo: "baz"});
322      collection.reset([m2]);
323    });
324
325    it("should render the template with the model", function(){
326      expect(compositeView.$el).toHaveText(/composite bar/);
327    });
328
329    it("should render the collection's items", function(){
330      expect(compositeView.$el).toHaveText(/baz/);
331    });
332  });
333
334  describe("when rendering a composite view without a collection", function(){
335    var compositeView;
336
337    var ItemView = Backbone.Marionette.ItemView.extend({
338      tagName: "span",
339      render: function(){
340        this.$el.html(this.model.get("foo"));
341      }
342    });
343
344    var CompositeView = Backbone.Marionette.CompositeView.extend({
345      itemView: ItemView,
346      template: "#composite-template",
347
348      onRender: function(){}
349    });
350
351    beforeEach(function(){
352      loadFixtures("compositeRerender.html");
353
354      var m1 = new Model({foo: "bar"});
355      compositeView = new CompositeView({
356        model: m1
357      });
358
359      compositeView.render();
360    });
361
362    it("should render the template with the model", function(){
363      expect(compositeView.$el).toHaveText(/composite bar/);
364    });
365
366    it("should not render the collection's items", function(){
367      expect(compositeView.$el).not.toHaveText(/baz/);
368    });
369  });
370
371  describe("when rendering a composite with a collection and then resetting the collection", function(){
372    var compositeView;
373
374    var ItemView = Backbone.Marionette.ItemView.extend({
375      tagName: "span",
376      render: function(){
377        this.$el.html(this.model.get("foo"));
378      }
379    });
380
381    var CompositeView = Backbone.Marionette.CompositeView.extend({
382      itemView: ItemView,
383      template: "#composite-template",
384
385      onRender: function(){}
386    });
387
388    beforeEach(function(){
389      loadFixtures("compositeRerender.html");
390
391      var m1 = new Model({foo: "bar"});
392      var m2 = new Model({foo: "baz"});
393      var collection = new Collection([m2]);
394
395      compositeView = new CompositeView({
396        model: m1,
397        collection: collection
398      });
399
400      compositeView.render();
401
402      spyOn(compositeView, "renderModel").andCallThrough();
403
404      var m3 = new Model({foo: "quux"});
405      var m4 = new Model({foo: "widget"});
406      collection.reset([m3, m4]);
407    });
408
409    it("should not re-render the template with the model", function(){
410      expect(compositeView.renderModel).not.toHaveBeenCalled();
411    });
412
413    it("should render the collection's items", function(){
414      expect(compositeView.$el).not.toHaveText(/baz/);
415      expect(compositeView.$el).toHaveText(/quux/);
416      expect(compositeView.$el).toHaveText(/widget/);
417    });
418  });
419
420  describe("when workign with a composite and recursive model", function(){
421    var treeView;
422
423    beforeEach(function(){
424      loadFixtures("recursiveCompositeTemplate.html");
425
426      var data = {
427        name: "level 1",
428        nodes: [
429          {
430            name: "level 2",
431            nodes: [
432              {
433                name: "level 3"
434              }
435            ]
436          }
437        ]
438      };
439
440      var node = new Node(data);
441      treeView = new TreeView({
442        model: node
443      });
444
445      treeView.render();
446    });
447
448    it("should render the template with the model", function(){
449      expect(treeView.$el).toHaveText(/level 1/);
450    });
451
452    it("should render the collection's items", function(){
453      expect(treeView.$el).toHaveText(/level 2/);
454    });
455
456    it("should render all the levels of the nested object", function(){
457      expect(treeView.$el).toHaveText(/level 3/);
458    });
459  });
460
461  describe("when closing a composite view", function(){
462    var compositeView, compositeModelCloseSpy;
463
464    var ItemView = Backbone.Marionette.ItemView.extend({
465      tagName: "span",
466      render: function(){
467        this.$el.html(this.model.get("foo"));
468      }
469    });
470
471    var CompositeModelView = Backbone.Marionette.CompositeView.extend({
472      itemView: ItemView,
473      template: "#composite-template"
474    });
475
476    beforeEach(function(){
477      loadFixtures("compositeTemplate.html");
478
479      var m1 = new Model({foo: "bar"});
480      var m2 = new Model({foo: "baz"});
481      var collection = new Collection();
482      collection.add(m2);
483
484      compositeView = new CompositeModelView({
485        model: m1,
486        collection: collection
487      });
488
489      spyOn(CompositeModelView.prototype, "close").andCallThrough();
490
491      compositeView.render();
492
493      compositeView.close();
494    });
495
496    it("should delete the model view", function(){
497      expect(compositeView.renderedModelView).toBeUndefined();
498    });
499
500    it("should close the collection of views", function(){
501      expect(CompositeModelView.prototype.close.callCount).toBe(1);
502    });
503  });
504
505  describe("when rendering a composite view with no model, using a template to create a grid", function(){
506
507    var gridView;
508
509    // A Grid Row
510    var GridRow = Backbone.Marionette.ItemView.extend({
511      tagName: "tr",
512      template: "#row-template"
513    });
514
515    // The grid view
516    var GridView = Backbone.Marionette.CompositeView.extend({
517      tagName: "table",
518      template: "#grid-template",
519      itemView: GridRow,
520
521      appendHtml: function(cv, iv){
522        cv.$("tbody").append(iv.el);
523      }
524    });
525
526    beforeEach(function(){
527      loadFixtures("gridTemplates.html");
528
529      var userData = [
530        {
531          username: "dbailey",
532          fullname: "Derick Bailey"
533        },
534        {
535          username: "jbob",
536        fullname: "Joe Bob"
537        },
538        {
539          username: "fbar",
540        fullname: "Foo Bar"
541        }
542      ];
543
544      var userList = new UserCollection(userData);
545
546      gridView = new GridView({
547        tagName: "table",
548        collection: userList
549      });
550
551      gridView.render();
552    });
553
554    it("should render the table", function(){
555      expect(gridView.$("th").length).not.toBe(0);
556    });
557
558    it("should render the users", function(){
559      var body = gridView.$("tbody");
560      expect(body).toHaveText(/dbailey/);
561      expect(body).toHaveText(/jbob/);
562      expect(body).toHaveText(/fbar/);
563    });
564  });
565
566  describe("when a composite view has a ui elements hash", function() {
567
568    var called, gridView, headersModel;
569
570    // A Grid Row
571    var GridRow = Backbone.Marionette.ItemView.extend({
572      tagName: "tr",
573      template: "#row-template"
574    });
575
576    // The grid view
577    var GridView = Backbone.Marionette.CompositeView.extend({
578      tagName: "table",
579      template: "#grid-template",
580      itemView: GridRow,
581
582      appendHtml: function(cv, iv){
583        cv.$("tbody").append(iv.el);
584      }
585    });
586
587    var GridViewWithUIBindings = GridView.extend({
588      template: "#ui-binding-template",
589
590      ui: {
591        headersRow: "thead tr",
592        unfoundElement: "#unfound",
593        itemRows: "tbody tr"
594      }
595    });
596
597    beforeEach(function() {
598      loadFixtures("uiBindingTemplate.html");
599
600      var userData = [
601        {
602          username: "dbailey",
603          fullname: "Derick Bailey"
604        },
605        {
606          username: "jbob",
607          fullname: "Joe Bob"
608        }
609      ];
610
611      headersModel = new Backbone.Model({
612        userHeader: "Username",
613        nameHeader: "Full name"
614      });
615
616      var userList = new UserCollection(userData);
617
618      gridView = new GridViewWithUIBindings({
619        tagName: "table",
620        model: headersModel,
621        collection: userList
622      });
623
624      // We don't render the view here since we need more fine-tuned control on when the view is rendered,
625      // specifically in the test that asserts the composite view template elements are accessible before
626      // the collection is rendered.
627    });
628
629    describe("after the whole composite view finished rendering", function() {
630
631      beforeEach(function() {
632        gridView.render();
633      });
634
635      describe("accessing a ui element that belongs to the model template", function() {
636
637        it("should return its jQuery selector if it can be found", function() {
638          expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
639        });
640
641        it("should return an empty jQuery object if it cannot be found", function() {
642          expect(gridView.ui.unfoundElement.length).toEqual(0);
643        });
644
645        it("should return an up-to-date selector on subsequent renders", function() {
646          // asserting state before subsequent render
647          expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("Username");
648
649          headersModel.set("userHeader", "User");
650          gridView.render();
651
652          expect(gridView.ui.headersRow.find("th:first-child")).toHaveText("User");
653        });
654
655      });
656
657      describe("accessing a ui element that belongs to the collection", function() {
658
659        // This test makes it clear that not allowing access to the collection elements is a design decision
660        // and not a bug.
661        it("should return an empty jQuery object", function() {
662          expect(gridView.ui.itemRows.length).toEqual(0);
663        });
664
665      });
666
667    });
668
669    describe("after the model finished rendering, but before the collection rendered", function() {
670
671      describe("accessing a ui element that belongs to the model template", function() {
672
673        beforeEach(function(){
674
675          gridView.onBeforeRender = function() {
676            called = true;
677          };
678          spyOn(gridView, "onBeforeRender").andCallThrough();
679
680          gridView.render();
681
682        });
683
684        // this test enforces that ui elements should be accessible as soon as their html was inserted
685        // to the DOM
686        it("should return its jQuery selector", function() {
687          expect(gridView.onBeforeRender).toHaveBeenCalled();
688        })
689
690        it("should set the username", function(){
691          expect($(gridView.ui.headersRow).find("th:first-child").text()).toEqual("Username");
692        });
693
694      });
695
696    });
697
698  });
699  
700  describe("has a valid inheritance chain back to Marionette.CollectionView", function(){
701    
702    var constructor;
703    
704    beforeEach(function(){
705      constructor = spyOn(Marionette.CollectionView.prototype, "constructor");
706      new Marionette.CompositeView();
707    });
708    
709    it("calls the parent Marionette.CollectionView's constructor function on instantiation", function(){
710      expect(constructor).toHaveBeenCalled();
711    });
712  });
713
714  // Models
715
716  var Model = Backbone.Model.extend({});
717
718  var User = Backbone.Model.extend({});
719
720  var Node = Backbone.Model.extend({
721    initialize: function(){
722      var nodes = this.get("nodes");
723      if (nodes){
724        this.nodes = new NodeCollection(nodes);
725        this.unset("nodes");
726      }
727    }
728  });
729
730  // Collections
731
732  var Collection = Backbone.Collection.extend({
733    model: Model
734  });
735
736  var UserCollection = Backbone.Collection.extend({
737    model: User
738  });
739
740  var NodeCollection = Backbone.Collection.extend({
741    model: Node
742  });
743
744  // Views
745
746  var TreeView = Backbone.Marionette.CompositeView.extend({
747    tagName: "ul",
748    template: "#recursive-composite-template",
749
750    initialize: function(){
751      this.collection = this.model.nodes;
752    }
753  });
754
755});