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