PageRenderTime 110ms CodeModel.GetById 2ms app.highlight 97ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/javascripts/collectionView.spec.js

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