PageRenderTime 93ms CodeModel.GetById 17ms app.highlight 70ms RepoModel.GetById 1ms app.codeStats 1ms

/spec/javascripts/compositeView.spec.js

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