PageRenderTime 4ms CodeModel.GetById 68ms app.highlight 74ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/collectionView.spec.js

https://github.com/4amitnarayan/backbone.marionette
JavaScript | 976 lines | 749 code | 218 blank | 9 comment | 20 complexity | cd0ca7a6aad95d0843ddc07502c1eb0c MD5 | raw file
  1describe('collection view', function() {
  2  'use strict';
  3
  4  beforeEach(function() {
  5    // Shared View Definitions
  6    // -----------------------
  7
  8    this.ChildView = Backbone.Marionette.ItemView.extend({
  9      tagName: 'span',
 10      render: function() {
 11        this.$el.html(this.model.get('foo'));
 12        this.trigger('render');
 13      },
 14      onRender: function() {}
 15    });
 16
 17    this.MockCollectionView = Backbone.Marionette.CollectionView.extend({
 18      childView: this.ChildView,
 19      onBeforeRender: function() {},
 20      onRender: function() {},
 21      onBeforeAddChild: function() {},
 22      onAddChild: function() {},
 23      onBeforeRemoveChild: function() {},
 24      onRemoveChild: function() {}
 25    });
 26  });
 27
 28  // Collection View Specs
 29  // ---------------------
 30
 31  describe('when rendering a collection view with no "childView" specified', function() {
 32    beforeEach(function() {
 33      this.NoChildView = Backbone.Marionette.CollectionView.extend();
 34
 35      this.collection = new Backbone.Collection([{foo:'bar'}, {foo: 'baz'}]);
 36      this.collectionView = new this.NoChildView({
 37        collection: this.collection
 38      });
 39    });
 40
 41    it('should throw an error saying theres not child view', function() {
 42      expect(this.collectionView.render).to.throw('A "childView" must be specified');
 43    });
 44  });
 45
 46  describe('when rendering a collection view', function() {
 47    beforeEach(function() {
 48      this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
 49
 50      this.childViewRender = this.sinon.stub();
 51
 52      this.collectionView = new this.MockCollectionView({
 53        collection: this.collection
 54      });
 55
 56      this.collectionView.on('childview:render', this.childViewRender);
 57
 58      this.sinon.spy(this.collectionView, 'onRender');
 59      this.sinon.spy(this.collectionView, 'onBeforeAddChild');
 60      this.sinon.spy(this.collectionView, 'onAddChild');
 61      this.sinon.spy(this.collectionView, 'onBeforeRender');
 62      this.sinon.spy(this.collectionView, 'trigger');
 63      this.sinon.spy(this.collectionView, 'attachHtml');
 64      this.sinon.spy(this.collectionView.$el, 'append');
 65      this.sinon.spy(this.collectionView, 'startBuffering');
 66      this.sinon.spy(this.collectionView, 'endBuffering');
 67
 68      this.collectionView.render();
 69    });
 70
 71    it('should only call $el.append once', function() {
 72      expect(this.collectionView.$el.append.callCount).to.equal(1);
 73    });
 74
 75    it('should only call clear render buffer once', function() {
 76      expect(this.collectionView.endBuffering.callCount).to.equal(1);
 77    });
 78
 79    it('should add to render buffer once for each child', function() {
 80      expect(this.collectionView.attachHtml.callCount).to.equal(2);
 81    });
 82
 83    it('should append the html for each childView', function() {
 84      expect($(this.collectionView.$el)).to.have.$html('<span>bar</span><span>baz</span>');
 85    });
 86
 87    it('should provide the index for each childView, when appending', function() {
 88      expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(0);
 89    });
 90
 91    it('should reference each of the rendered view children', function() {
 92      expect(_.size(this.collectionView.children)).to.equal(2);
 93    });
 94
 95    it('should call "onBeforeRender" before rendering', function() {
 96      expect(this.collectionView.onBeforeRender).to.have.been.called;
 97    });
 98
 99    it('should call "onRender" after rendering', function() {
100      expect(this.collectionView.onRender).to.have.been.called;
101    });
102
103    it('should trigger a "before:render" event', function() {
104      expect(this.collectionView.trigger).to.have.been.calledWith('before:render', this.collectionView);
105    });
106
107    it('should trigger a "before:render:collection" event', function() {
108      expect(this.collectionView.trigger).to.have.been.calledWith('before:render:collection', this.collectionView);
109    });
110
111    it('should trigger a "render:collection" event', function() {
112      expect(this.collectionView.trigger).to.have.been.calledWith('render:collection', this.collectionView);
113    });
114
115    it('should trigger a "render" event', function() {
116      expect(this.collectionView.trigger).to.have.been.calledWith('render', this.collectionView);
117    });
118
119    it('should call "onBeforeAddChild" for each childView instance', function() {
120      var v1 = this.collectionView.children.findByIndex(0);
121      var v2 = this.collectionView.children.findByIndex(1);
122      expect(this.collectionView.onBeforeAddChild).to.have.been.calledWith(v1);
123      expect(this.collectionView.onBeforeAddChild).to.have.been.calledWith(v2);
124    });
125
126    it('should call "onAddChild" for each childView instance', function() {
127      var v1 = this.collectionView.children.findByIndex(0);
128      var v2 = this.collectionView.children.findByIndex(1);
129      expect(this.collectionView.onAddChild).to.have.been.calledWith(v1);
130      expect(this.collectionView.onAddChild).to.have.been.calledWith(v2);
131    });
132
133    it('should call "onBeforeAddChild" for all childView instances', function() {
134      expect(this.collectionView.onBeforeAddChild.callCount).to.equal(2);
135    });
136
137    it('should call "onAddChild" for all childView instances', function() {
138      expect(this.collectionView.onAddChild.callCount).to.equal(2);
139    });
140
141    it('should trigger "childview:render" for each item in the collection', function() {
142      expect(this.childViewRender.callCount).to.equal(2);
143    });
144  });
145
146  describe('when rendering a collection view without a collection', function() {
147    beforeEach(function() {
148      this.collectionView = new this.MockCollectionView();
149
150      this.sinon.spy(this.collectionView, 'onRender');
151      this.sinon.spy(this.collectionView, 'onBeforeRender');
152      this.sinon.spy(this.collectionView, 'trigger');
153
154      this.collectionView.render();
155    });
156
157    it('should not append any html', function() {
158      expect($(this.collectionView.$el)).not.to.have.$html('<span>bar</span><span>baz</span>');
159    });
160
161    it('should not reference any view children', function() {
162      expect(this.collectionView.children.length).to.equal(0);
163    });
164  });
165
166  describe('when a model is added to the collection', function() {
167    beforeEach(function() {
168      this.collection = new Backbone.Collection();
169      this.collectionView = new this.MockCollectionView({
170        childView: this.ChildView,
171        collection: this.collection
172      });
173      this.collectionView.render();
174
175      this.childViewRender = this.sinon.stub();
176      this.collectionView.on('childview:render', this.childViewRender);
177
178      this.sinon.spy(this.collectionView, 'attachHtml');
179
180      this.model = new Backbone.Model({foo: 'bar'});
181      this.collection.add(this.model);
182    });
183
184    it('should add the model to the list', function() {
185      expect(_.size(this.collectionView.children)).to.equal(1);
186    });
187
188    it('should render the model in to the DOM', function() {
189      expect($(this.collectionView.$el)).to.contain.$text('bar');
190    });
191
192    it('should provide the index for each childView, when appending', function() {
193      expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(0);
194    });
195
196    it('should trigger the childview:render event from the collectionView', function() {
197      expect(this.childViewRender).to.have.been.called;
198    });
199  });
200
201  describe('when a model is added to a non-empty collection', function() {
202    beforeEach(function() {
203      this.collection = new Backbone.Collection({foo: 'bar'});
204
205      this.collectionView = new this.MockCollectionView({
206        childView: this.ChildView,
207        collection: this.collection
208      });
209      this.collectionView.render();
210
211      this.childViewRender = this.sinon.stub();
212      this.collectionView.on('childview:render', this.childViewRender);
213
214      this.sinon.spy(this.collectionView, 'attachHtml');
215
216      this.model = new Backbone.Model({foo: 'baz'});
217      this.collection.add(this.model);
218    });
219
220    it('should add the model to the list', function() {
221      expect(_.size(this.collectionView.children)).to.equal(2);
222    });
223
224    it('should render the model in to the DOM', function() {
225      expect($(this.collectionView.$el)).to.contain.$text('barbaz');
226    });
227
228    it('should provide the index for each child view, when appending', function() {
229      expect(this.collectionView.attachHtml.firstCall.args[2]).to.equal(1);
230    });
231
232    it('should trigger the childview:render event from the collectionView', function() {
233      expect(this.childViewRender).to.have.been.called;
234    });
235  });
236
237  describe('when providing a custom render that adds children, without a collection object to use, and removing a child', function() {
238    beforeEach(function() {
239      var suite = this;
240
241      this.model = new Backbone.Model({foo: 'bar'});
242
243      this.EmptyView = Backbone.Marionette.ItemView.extend({
244        render: function() {}
245      });
246
247      this.CollectionView = Backbone.Marionette.CollectionView.extend({
248        childView: this.ChildView,
249        emptyView: this.EmptyView,
250
251        onBeforeRenderEmpty: function() {},
252        onRenderEmpty: function() {},
253
254        render: function() {
255          var ChildView = this.getChildView();
256          this.addChild(suite.model, ChildView, 0);
257        }
258      });
259
260      this.collectionView = new this.CollectionView({});
261      this.collectionView.render();
262
263      this.childView = this.collectionView.children.findByIndex(0);
264
265      this.beforeRenderSpy = this.sinon.spy(this.collectionView, 'onBeforeRenderEmpty');
266      this.renderSpy = this.sinon.spy(this.collectionView, 'onRenderEmpty');
267
268      this.sinon.spy(this.childView, 'destroy');
269      this.sinon.spy(this.EmptyView.prototype, 'render');
270
271      this.collectionView._onCollectionRemove(this.model);
272    });
273
274    it('should destroy the models view', function() {
275      expect(this.childView.destroy).to.have.been.called;
276    });
277
278    it('should show the empty view', function() {
279      expect(this.EmptyView.prototype.render.callCount).to.equal(1);
280    });
281
282    it('should call "onBeforeRenderEmpty"', function() {
283      expect(this.beforeRenderSpy).to.have.been.called;
284    });
285
286    it('should call "onRenderEmpty"', function() {
287      expect(this.renderSpy).to.have.been.called;
288    });
289  });
290
291  describe('when a model is removed from the collection', function() {
292    beforeEach(function() {
293      this.model = new Backbone.Model({foo: 'bar'});
294      this.collection = new Backbone.Collection();
295      this.collection.add(this.model);
296
297      this.collectionView = new this.MockCollectionView({
298        childView: this.ChildView,
299        collection: this.collection
300      });
301      this.collectionView.render();
302
303      this.childView = this.collectionView.children.findByIndex(0);
304
305      this.sinon.spy(this.childView, 'destroy');
306
307      this.onBeforeRemoveChildSpy = this.sinon.spy(this.collectionView, 'onBeforeRemoveChild');
308      this.onRemoveChildSpy = this.sinon.spy(this.collectionView, 'onRemoveChild');
309
310      this.collection.remove(this.model);
311    });
312
313    it('should destroy the models view', function() {
314      expect(this.childView.destroy).to.have.been.called;
315    });
316
317    it('should remove the model-views HTML', function() {
318      expect($(this.collectionView.$el).children().length).to.equal(0);
319    });
320
321    it('should execute onBeforeRemoveChild', function() {
322      expect(this.onBeforeRemoveChildSpy).to.have.been.calledOnce;
323    });
324
325    it('should pass the removed view to onBeforeRemoveChild', function() {
326      expect(this.onBeforeRemoveChildSpy).to.have.been.calledWithExactly(this.childView);
327    });
328
329    it('should execute onRemoveChild', function() {
330      expect(this.onRemoveChildSpy).to.have.been.calledOnce;
331    });
332
333    it('should pass the removed view to _onCollectionRemove', function() {
334      expect(this.onRemoveChildSpy).to.have.been.calledWithExactly(this.childView);
335    });
336
337    it('should execute onBeforeRemoveChild before _onCollectionRemove', function() {
338      expect(this.onBeforeRemoveChildSpy).to.have.been.calledBefore(this.onRemoveChildSpy);
339    });
340  });
341
342  describe('when destroying a collection view', function() {
343    beforeEach(function() {
344      this.EventedView = Backbone.Marionette.CollectionView.extend({
345        childView: this.ChildView,
346        someCallback: function() {},
347        onBeforeDestroy: function() {},
348        onDestroy: function() {}
349      });
350
351      this.destroyHandler = this.sinon.stub();
352
353      this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
354      this.collectionView = new this.EventedView({
355        template: '#itemTemplate',
356        collection: this.collection
357      });
358      this.collectionView.someItemViewCallback = function() {};
359      this.collectionView.render();
360
361
362      this.childModel = this.collection.at(0);
363      this.childView = this.collectionView.children.findByIndex(0);
364
365      this.collectionView.listenTo(this.collection, 'foo', this.collectionView.someCallback);
366      this.collectionView.listenTo(this.collectionView, 'item:foo', this.collectionView.someItemViewCallback);
367
368      this.sinon.spy(this.childView, 'destroy');
369      this.sinon.spy(this.collectionView, '_onCollectionRemove');
370      this.sinon.spy(this.collectionView, 'stopListening');
371      this.sinon.spy(this.collectionView, 'remove');
372      this.sinon.spy(this.collectionView, 'someCallback');
373      this.sinon.spy(this.collectionView, 'someItemViewCallback');
374      this.sinon.spy(this.collectionView, 'destroy');
375      this.sinon.spy(this.collectionView, 'onDestroy');
376      this.sinon.spy(this.collectionView, 'onBeforeDestroy');
377      this.sinon.spy(this.collectionView, 'trigger');
378
379      this.collectionView.bind('destroy:collection', this.destroyHandler);
380
381      this.collectionView.destroy();
382
383      this.childView.trigger('foo');
384
385      this.collection.trigger('foo');
386      this.collection.remove(this.childModel);
387    });
388
389    it('should destroy all of the child views', function() {
390      expect(this.childView.destroy).to.have.been.called;
391    });
392
393    it('should unbind all the listenTo events', function() {
394      expect(this.collectionView.stopListening).to.have.been.called;
395    });
396
397    it('should unbind all collection events for the view', function() {
398      expect(this.collectionView.someCallback).not.to.have.been.called;
399    });
400
401    it('should unbind all item-view events for the view', function() {
402      expect(this.collectionView.someItemViewCallback).not.to.have.been.called;
403    });
404
405    it('should not retain any references to its children', function() {
406      expect(_.size(this.collectionView.children)).to.equal(0);
407    });
408
409    it('should unbind any listener to custom view events', function() {
410      expect(this.collectionView.stopListening).to.have.been.called;
411    });
412
413    it('should remove the views EL from the DOM', function() {
414      expect(this.collectionView.remove).to.have.been.called;
415    });
416
417    it('should call "onDestroy" if provided', function() {
418      expect(this.collectionView.onDestroy).to.have.been.called;
419    });
420
421    it('should call "onBeforeDestroy" if provided', function() {
422      expect(this.collectionView.onBeforeDestroy).to.have.been.called;
423    });
424
425    it('should trigger a "before:destroy" event', function() {
426      expect(this.collectionView.trigger).to.have.been.calledWith('before:destroy:collection');
427    });
428
429    it('should trigger a "destroy"', function() {
430      expect(this.collectionView.trigger).to.have.been.calledWith('destroy:collection');
431    });
432
433    it('should call the handlers add to the destroyed event', function() {
434      expect(this.destroyHandler).to.have.been.called;
435    });
436
437    it('should throw an error saying the views been destroyed if render is attempted again', function() {
438      expect(this.collectionView.render).to.throw('Cannot use a view thats already been destroyed.');
439    });
440  });
441
442  describe('when destroying an childView that does not have a "destroy" method', function() {
443    beforeEach(function() {
444      this.collectionView = new Marionette.CollectionView({
445        childView: Backbone.View,
446        collection: new Backbone.Collection([{id: 1}])
447      });
448
449      this.collectionView.render();
450
451      this.childView = this.collectionView.children.findByIndex(0);
452      this.sinon.spy(this.childView, 'remove');
453
454      this.collectionView.destroyChildren();
455    });
456
457    it('should call the "remove" method', function() {
458      expect(this.childView.remove).to.have.been.called;
459    });
460  });
461
462  describe('when override attachHtml', function() {
463    beforeEach(function() {
464      this.PrependHtmlView = Backbone.Marionette.CollectionView.extend({
465        childView: this.ChildView,
466
467        attachHtml: function(collectionView, childView) {
468          collectionView.$el.prepend(childView.el);
469        }
470      });
471
472      this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);
473
474      this.collectionView = new this.PrependHtmlView({
475        collection: this.collection
476      });
477
478      this.collectionView.render();
479    });
480
481    it('should append via the overridden method', function() {
482      expect(this.collectionView.$el).to.contain.$html('<span>baz</span><span>bar</span>');
483    });
484  });
485
486  describe('when a child view triggers an event', function() {
487    beforeEach(function() {
488      this.someEventSpy = this.sinon.stub();
489
490      this.model = new Backbone.Model({foo: 'bar'});
491      this.collection = new Backbone.Collection([this.model]);
492
493      this.collectionView = new this.MockCollectionView({collection: this.collection});
494      this.collectionView.on('childview:some:event', this.someEventSpy);
495      this.collectionView.render();
496
497      this.sinon.spy(this.collectionView, 'trigger');
498      this.childView = this.collectionView.children.findByIndex(0);
499      this.childView.trigger('some:event', 'test', this.model);
500    });
501
502    it('should bubble up through the parent collection view', function() {
503      expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
504    });
505
506    it('should provide the child view that triggered the event, including other relevant parameters', function() {
507      expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
508    });
509  });
510
511  describe('when configuring a custom childViewEventPrefix', function() {
512    beforeEach(function() {
513      this.CollectionView = this.MockCollectionView.extend({
514        childViewEventPrefix: 'myPrefix'
515      });
516
517      this.someEventSpy = this.sinon.stub();
518
519      this.model = new Backbone.Model({foo: 'bar'});
520      this.collection = new Backbone.Collection([this.model]);
521
522      this.collectionView = new this.CollectionView({collection: this.collection});
523      this.collectionView.on('myPrefix:some:event', this.someEventSpy);
524      this.collectionView.render();
525
526      this.sinon.spy(this.collectionView, 'trigger');
527      this.childView = this.collectionView.children.findByIndex(0);
528      this.childView.trigger('some:event', 'test', this.model);
529    });
530
531    it('should bubble up through the parent collection view', function() {
532      expect(this.collectionView.trigger).to.have.been.calledWith('myPrefix:some:event', this.childView, 'test', this.model);
533    });
534
535    it('should provide the child view that triggered the event, including other relevant parameters', function() {
536      expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
537    });
538  });
539
540  describe('when a child view triggers the default', function() {
541    beforeEach(function() {
542      this.model = new Backbone.Model({foo: 'bar'});
543      this.collection = new Backbone.Collection([this.model]);
544
545      this.collectionView = new this.MockCollectionView({
546        childView: Backbone.Marionette.ItemView.extend({
547          template: function() { return '<%= foo %>'; }
548        }),
549        collection: this.collection
550      });
551    });
552
553    describe('render events', function() {
554      beforeEach(function() {
555        this.beforeSpy = this.sinon.stub();
556        this.renderSpy = this.sinon.stub();
557
558        this.collectionView.on('childview:before:render', this.beforeSpy);
559        this.collectionView.on('childview:render', this.renderSpy);
560
561        this.collectionView.render();
562        this.childView = this.collectionView.children.findByIndex(0);
563      });
564
565      it('should bubble up through the parent collection view', function() {
566        // As odd as it seems, the events are triggered with two arguments,
567        // the first being the child view which triggered the event
568        // and the second being the event's owner.  It just so happens to be the
569        // same view.
570        expect(this.beforeSpy).to.have.been.calledWith(this.childView, this.childView);
571        expect(this.renderSpy).to.have.been.calledWith(this.childView, this.childView);
572      });
573    });
574
575    describe('destroy events', function() {
576      beforeEach(function() {
577        this.beforeSpy = this.sinon.stub();
578        this.destroySpy = this.sinon.stub();
579
580        this.collectionView.on('childview:before:destroy', this.beforeSpy);
581        this.collectionView.on('childview:destroy', this.destroySpy);
582
583        this.collectionView.render();
584        this.childView = this.collectionView.children.findByIndex(0);
585        this.collectionView.destroy();
586      });
587
588      it('should bubble up through the parent collection view', function() {
589        expect(this.beforeSpy).to.have.been.calledWith(this.childView);
590        expect(this.destroySpy).to.have.been.calledWith(this.childView);
591      });
592    });
593  });
594
595  describe('when a child view is removed from a collection view', function() {
596    beforeEach(function() {
597      this.model = new Backbone.Model({foo: 'bar'});
598      this.collection = new Backbone.Collection([this.model]);
599
600      this.collectionView = new this.MockCollectionView({
601        template: '#itemTemplate',
602        collection: this.collection
603      });
604
605      this.collectionView.render();
606
607      this.childView = this.collectionView.children[this.model.cid];
608      this.collection.remove(this.model);
609    });
610
611    it('should not retain any bindings to this view', function() {
612      var suite = this;
613      var bindings = this.collectionView.bindings || {};
614      expect(_.any(bindings, function(binding) {
615        return binding.obj === suite.childView;
616      })).to.be.false;
617    });
618
619    it('should not retain any references to this view', function() {
620      expect(_.size(this.collectionView.children)).to.equal(0);
621    });
622  });
623
624  describe('when the collection of a collection view is reset', function() {
625    beforeEach(function() {
626      this.model = new Backbone.Model({foo: 'bar'});
627      this.collection = new Backbone.Collection([ this.model ]);
628
629      this.collectionView = new this.MockCollectionView({
630        template: '#itemTemplate',
631        collection: this.collection
632      });
633
634      this.collectionView.render();
635
636      this.childView = this.collectionView.children[this.model.cid];
637      this.collection.reset();
638    });
639
640    it('should not retain any references to the previous views', function() {
641      expect(_.size(this.collectionView.children)).to.equal(0);
642    });
643
644    it('should not retain any bindings to the previous views', function() {
645      var suite = this;
646      var bindings = this.collectionView.bindings || {};
647      expect(_.any(bindings, function(binding) {
648        return binding.obj === suite.childView;
649      })).to.be.false;
650    });
651  });
652
653  describe('when a child view is added to a collection view, after the collection view has been shown', function() {
654    beforeEach(function() {
655      this.ChildView = Backbone.Marionette.ItemView.extend({
656        onShow: function() {},
657        onDomRefresh: function() {},
658        onRender: function() {},
659        render: function() {
660          this.trigger('render');
661        }
662      });
663
664      this.CollectionView = Backbone.Marionette.CollectionView.extend({
665        childView: this.ChildView,
666        onShow: function() {}
667      });
668
669      this.sinon.spy(this.ChildView.prototype, 'onShow');
670      this.sinon.spy(this.ChildView.prototype, 'onDomRefresh');
671
672      this.model1 = new Backbone.Model();
673      this.model2 = new Backbone.Model();
674      this.collection = new Backbone.Collection([ this.model1 ]);
675      this.collectionView = new this.CollectionView({
676        collection: this.collection
677      });
678      $('body').append(this.collectionView.el);
679
680      this.collectionView.render();
681      this.collectionView.onShow();
682      this.collectionView.trigger('show');
683
684      this.sinon.spy(this.collectionView, 'attachBuffer');
685
686      this.collection.add(this.model2);
687      this.view = this.collectionView.children.findByIndex(1);
688    });
689
690    it('should not use the render buffer', function() {
691      expect(this.collectionView.attachBuffer).not.to.have.been.called;
692    });
693
694    it('should call the "onShow" method of the child view', function() {
695      expect(this.ChildView.prototype.onShow).to.have.been.called;
696    });
697
698    it('should call the childs "onShow" method with itself as the context', function() {
699      expect(this.ChildView.prototype.onShow).to.have.been.calledOn(this.view);
700    });
701
702    it('should call the childs "onDomRefresh" method with itself as the context', function() {
703      expect(this.ChildView.prototype.onDomRefresh).to.have.been.called;
704    });
705  });
706
707  describe('when setting an childView in the constructor options', function() {
708    beforeEach(function() {
709      this.ItemView = Marionette.ItemView.extend({
710        template: function() {},
711        MyItemView: true
712      });
713
714      this.collection = new Backbone.Collection([{a: 'b'}]);
715      this.collectionView = new Marionette.CollectionView({
716        childView: this.ItemView,
717        collection: this.collection
718      });
719
720      this.collectionView.render();
721
722      this.itemView = this.collectionView.children.findByModel(this.collection.at(0));
723    });
724
725    it('should use the specified childView for each item', function() {
726      expect(this.itemView.MyItemView).to.be.true;
727    });
728  });
729
730  describe('when calling childEvents via an childEvents method', function() {
731    beforeEach(function() {
732      this.CollectionView = this.MockCollectionView.extend({
733        childEvents: function() {
734          return {
735            'some:event': 'someEvent'
736          };
737        }
738      });
739
740      this.someEventSpy = this.sinon.stub();
741
742      this.model = new Backbone.Model({foo: 'bar'});
743      this.collection = new Backbone.Collection([this.model]);
744
745      this.collectionView = new this.CollectionView({collection: this.collection});
746      this.collectionView.someEvent = this.someEventSpy;
747      this.collectionView.render();
748
749      this.sinon.spy(this.collectionView, 'trigger');
750      this.childView = this.collectionView.children.findByIndex(0);
751      this.childView.trigger('some:event', 'test', this.model);
752    });
753
754    it('should bubble up through the parent collection view', function() {
755      expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
756    });
757
758    it('should provide the child view that triggered the event, including other relevant parameters', function() {
759      expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
760    });
761  });
762
763  describe('when calling childEvents via the childEvents hash', function() {
764    beforeEach(function() {
765      this.onSomeEventSpy = this.sinon.stub();
766
767      this.CollectionView = this.MockCollectionView.extend({
768        childEvents: {
769          'some:event': this.onSomeEventSpy
770        }
771      });
772
773      this.model = new Backbone.Model({foo: 'bar'});
774      this.collection = new Backbone.Collection([this.model]);
775
776      this.collectionView = new this.CollectionView({collection: this.collection});
777      this.collectionView.render();
778
779      this.sinon.spy(this.collectionView, 'trigger');
780      this.childView = this.collectionView.children.findByIndex(0);
781      this.childView.trigger('some:event', 'test', this.model);
782    });
783
784    it('should bubble up through the parent collection view', function() {
785      expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
786    });
787
788    it('should provide the child view that triggered the event, including other relevant parameters', function() {
789      expect(this.onSomeEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
790    });
791  });
792
793  describe('when calling childEvents via the childEvents hash with a string of the function name', function() {
794    beforeEach(function() {
795      this.CollectionView = this.MockCollectionView.extend({
796        childEvents: {
797          'some:event': 'someEvent'
798        }
799      });
800
801      this.someEventSpy = this.sinon.stub();
802
803      this.model = new Backbone.Model({foo: 'bar'});
804      this.collection = new Backbone.Collection([this.model]);
805
806      this.collectionView = new this.CollectionView({collection: this.collection});
807      this.collectionView.someEvent = this.someEventSpy;
808      this.collectionView.render();
809
810      this.sinon.spy(this.collectionView, 'trigger');
811      this.childView = this.collectionView.children.findByIndex(0);
812      this.childView.trigger('some:event', 'test', this.model);
813    });
814
815    it('should bubble up through the parent collection view', function() {
816      expect(this.collectionView.trigger).to.have.been.calledWith('childview:some:event', this.childView, 'test', this.model);
817    });
818
819    it('should provide the child view that triggered the event, including other relevant parameters', function() {
820      expect(this.someEventSpy).to.have.been.calledWith(this.childView, 'test', this.model);
821    });
822  });
823
824  describe('calling childEvents via the childEvents hash with a string of a nonexistent function name', function() {
825    beforeEach(function() {
826      this.CollectionView = Marionette.CollectionView.extend({
827        childView: this.ChildView,
828        childEvents: {
829          'render': 'nonexistentFn'
830        }
831      });
832
833      this.collectionView = new this.CollectionView({
834        collection: (new Backbone.Collection([{}]))
835      });
836
837      this.collectionView.render();
838    });
839
840    it('should not break', function() {
841      // Intentionally left blank
842    });
843  });
844
845  describe('has a valid inheritance chain back to Marionette.View', function() {
846    beforeEach(function() {
847      this.constructor = this.sinon.spy(Marionette, 'View');
848      this.collectionView = new Marionette.CollectionView();
849    });
850
851    it('calls the parent Marionette.Views constructor function on instantiation', function() {
852      expect(this.constructor).to.have.been.called;
853    });
854  });
855
856  describe('when a collection is reset child views should not be shown until the buffering is over', function() {
857    beforeEach(function() {
858      var suite = this;
859
860      this.ItemView = Marionette.ItemView.extend({
861        template: _.template('<div>hi mom</div>'),
862        onShow: function() {
863          suite.isBuffering = suite.collectionView.isBuffering;
864        }
865      });
866
867      this.CollectionView = Marionette.CollectionView.extend({
868        childView: this.ItemView
869      });
870
871      this.isBuffering = null;
872      this.collection = new Backbone.Collection([{}]);
873      this.collectionView = new this.CollectionView({collection: this.collection});
874      this.collectionView.render().trigger('show');
875    });
876
877    it('collectionView should not be buffering on childView show', function() {
878      expect(this.isBuffering).to.be.false;
879    });
880
881    it('collectionView should not be buffering after reset on childView show', function() {
882      this.isBuffering = undefined;
883      this.collection.reset([{}]);
884      expect(this.isBuffering).to.be.false;
885    });
886
887    describe('child view show events', function() {
888      beforeEach(function() {
889        var suite = this;
890        this.showCalled = false;
891        this.ItemView.prototype.onShow = function() { suite.showCalled = true; };
892      });
893
894      it('collectionView should trigger the show events when the buffer is inserted and the view has been shown', function() {
895        this.collection.reset([{}]);
896        expect(this.showCalled).to.equal(true);
897      });
898
899      it('collectionView should not trigger the show events if the view has not been shown', function() {
900        this.collectionView = new this.CollectionView({collection: this.collection});
901        this.collectionView.render();
902        expect(this.showCalled).to.equal(false);
903      });
904    });
905  });
906
907  describe('when a collection view is not rendered', function() {
908    beforeEach(function() {
909      var suite = this;
910      this.Model       = Backbone.Model.extend({});
911      this.Collection  = Backbone.Collection.extend({model: this.Model});
912      this.CollectionView = Backbone.Marionette.CollectionView.extend({
913        childView: this.ChildView,
914        tagName: 'ul'
915      });
916
917      this.addModel = function() {
918        suite.collection.add(suite.model2);
919      };
920
921      this.removeModel = function() {
922        suite.collection.remove(suite.model1);
923      };
924
925      this.resetCollection = function() {
926        suite.collection.reset([suite.model1, suite.model2]);
927      };
928
929      this.sync = function() {
930        suite.collection.trigger('sync');
931      };
932
933      this.model1     = new this.Model({monkey: 'island'});
934      this.model2     = new this.Model({lechuck: 'tours'});
935      this.collection = new this.Collection([this.model1]);
936
937      this.collectionView = new this.CollectionView({
938        collection: this.collection
939      });
940    });
941
942    it('should not fail when adding models to an unrendered collectionView', function() {
943      expect(this.addModel).not.to.throw;
944    });
945
946    it('should not fail when an item is removed from an unrendered collectionView', function() {
947      expect(this.removeModel).not.to.throw;
948    });
949
950    it('should not fail when a collection is reset on an unrendered collectionView', function() {
951      expect(this.resetCollection).not.to.throw;
952    });
953
954    it('should not fail when a collection is synced on an unrendered collectionView', function() {
955      expect(this.sync).not.to.throw;
956    });
957  });
958
959  describe('when returning the view from addChild', function() {
960    beforeEach(function() {
961      this.model = new Backbone.Model({foo: 'bar'});
962
963      this.CollectionView = Backbone.Marionette.CollectionView.extend({
964        childView: this.ChildView
965      });
966
967      this.collectionView = new this.CollectionView();
968
969      this.childView = this.collectionView.addChild(this.model, this.ChildView, 0);
970    });
971
972    it('should return the child view for the model', function() {
973      expect(this.childView.$el).to.contain.$text('bar');
974    });
975  });
976});