k.backbone.marionette
Version:
Make your Backbone.js apps dance!
775 lines (633 loc) • 24.2 kB
JavaScript
describe('Behaviors', function() {
'use strict';
describe('behavior lookup', function() {
it('should throw if behavior lookup is not defined', function() {
expect(function() {
Marionette.Behaviors.behaviorsLookup();
}).to.throw(Marionette.Error, new Marionette.Error({
message: 'You must define where your behaviors are stored.',
url: 'marionette.behaviors.md#behaviorslookup'
}));
});
});
describe('behavior parsing with a functional behavior lookup', function() {
beforeEach(function() {
this.behaviors = {
foo: this.sinon.spy(Marionette, "Behavior")
};
Marionette.Behaviors.behaviorsLookup = _.constant(this.behaviors);
});
describe('when one behavior', function() {
beforeEach(function() {
this.View = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
});
it('should instantiate the behavior', function() {
expect(this.behaviors.foo).to.have.been.calledOnce;
});
});
});
describe('behavior parsing', function() {
beforeEach(function() {
this.behaviors = {
foo: this.sinon.spy(Marionette, "Behavior")
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
});
describe('when one behavior', function() {
beforeEach(function() {
this.View = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
});
it('should instantiate the behavior', function() {
expect(this.behaviors.foo).to.have.been.calledOnce;
});
});
describe('when multiple behaviors', function() {
beforeEach(function() {
this.View = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
});
it('should instantiate the behavior', function() {
expect(this.behaviors.foo).to.have.been.calledOnce;
});
});
describe('when functional behavior', function() {
beforeEach(function() {
this.behaviorsStub = this.sinon.stub().returns({
foo: { behaviorClass: this.behaviors.foo }
});
this.View = Marionette.ItemView.extend({
behaviors: this.behaviorsStub
});
this.view = new this.View();
});
it('should instantiate the behavior', function() {
expect(this.behaviors.foo).to.have.been.calledOnce;
});
it('should call the behaviors method with the view context', function() {
expect(this.behaviorsStub).to.have.been.calledOnce.and.calledOn(this.view);
});
});
describe('when behavior class is provided', function() {
beforeEach(function() {
this.View = Marionette.ItemView.extend({
behaviors: { foo: { behaviorClass: this.behaviors.foo } }
});
this.view = new this.View();
});
it('should instantiate the behavior', function() {
expect(this.behaviors.foo).to.have.been.calledOnce;
});
});
});
describe('behavior initialize', function() {
beforeEach(function() {
this.behaviorOptions = { foo: 'bar' };
this.initializeStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({ initialize: this.initializeStub })
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.ItemView.extend({
behaviors: { foo: this.behaviorOptions }
});
this.view = new this.View();
});
it('should call initialize when a behavior is created', function() {
expect(this.initializeStub).to.have.been.calledOnce;
});
it('should call initialize when a behavior is created', function() {
expect(this.initializeStub).to.have.been.calledOnce.and.calledWith(this.behaviorOptions, this.view);
});
it('should set _behaviors', function() {
expect(this.view._behaviors.length).to.be.equal(1);
});
});
describe('behavior events', function() {
beforeEach(function() {
this.fooClickStub = this.sinon.stub();
this.barClickStub = this.sinon.stub();
this.bazClickStub = this.sinon.stub();
this.viewClickStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
events: { 'click': this.fooClickStub }
}),
bar: Marionette.Behavior.extend({
events: { 'click': this.barClickStub }
}),
baz: Marionette.Behavior.extend({
events: { 'click': 'handleClick' },
handleClick: this.bazClickStub
})
};
this.View = Marionette.ItemView.extend({
events: { 'click': this.viewClickStub },
behaviors: { foo: {}, bar: {}, baz: {} }
});
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.view = new this.View();
this.view.$el.click();
});
it('should call first behaviors event', function() {
expect(this.fooClickStub).to.have.been.calledOnce.and.calledOn(sinon.match.instanceOf(this.behaviors.foo));
});
it('should call second behaviors event', function() {
expect(this.barClickStub).to.have.been.calledOnce.and.calledOn(sinon.match.instanceOf(this.behaviors.bar));
});
it('should call third behaviors event', function() {
expect(this.bazClickStub).to.have.been.calledOnce.and.calledOn(sinon.match.instanceOf(this.behaviors.baz));
});
it('should call the view click handler', function() {
expect(this.viewClickStub).to.have.been.calledOnce.and.calledOn(this.view);
});
});
describe('behavior triggers', function() {
beforeEach(function() {
this.onClickFooStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
triggers: { 'click': 'click:foo' },
onClickFoo: this.onClickFooStub
})
};
this.model = new Backbone.Model();
this.collection = new Backbone.Collection();
this.View = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.view = new this.View({
model: this.model,
collection: this.collection
});
this.triggerMethodSpy = this.sinon.spy();
this.view.on('click:foo', this.triggerMethodSpy);
this.view.$el.click();
});
it('calls `triggerMethod` with the triggered event', function() {
expect(this.triggerMethodSpy)
.to.have.been.calledOnce
.and.calledOn(this.view);
});
it('calls the triggered method', function() {
expect(this.onClickFooStub)
.to.have.been.calledOnce
.and.have.been.calledOn(sinon.match.instanceOf(this.behaviors.foo));
});
});
describe('behavior $el', function() {
beforeEach(function() {
var suite = this;
this.behaviors = {
foo: Marionette.Behavior.extend({
initialize: function () { suite.fooBehavior = this; }
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
this.view.setElement(document.createElement('bar'));
});
it('should proxy the views $el', function() {
expect(this.fooBehavior.$el).to.equal(this.view.$el);
});
it('should proxy the views el', function() {
expect(this.fooBehavior.el).to.equal(this.view.el);
});
});
describe('behavior UI', function() {
beforeEach(function() {
var suite = this;
this.onRenderStub = this.sinon.stub();
this.onBeforeShowStub = this.sinon.stub();
this.onShowStub = this.sinon.stub();
this.onDestroyStub = this.sinon.stub();
this.onFooClickStub = this.sinon.stub();
this.onBarClickStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
ui: { foo: '.foo' },
initialize: function() { suite.fooBehavior = this; },
events: {
'click @ui.foo': 'onFooClick',
'click @ui.bar': 'onBarClick'
},
onRender : this.onRenderStub,
onBeforeShow : this.onBeforeShowStub,
onShow : this.onShowStub,
onDestroy : this.onDestroyStub,
onFooClick : this.onFooClickStub,
onBarClick : this.onBarClickStub
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.ItemView.extend({
template: _.template('<div class="foo"></div><div class="bar"></div>'),
ui: { bar: '.bar' },
behaviors: { foo: {} }
});
});
describe('should call onShow when inside a CollectionView', function() {
beforeEach(function() {
this.CollectionView = Marionette.CollectionView.extend({
childView: this.View
});
this.collection = new Backbone.Collection([{}]);
this.collectionView = new this.CollectionView({collection: this.collection});
this.collectionView.render();
this.collectionView.triggerMethod('show');
});
it('should call onShow when inside a CollectionView', function() {
expect(this.onShowStub).to.have.been.called;
});
it('should call onShow when already shown and reset', function() {
this.collection.reset([{id:1}, {id: 2}]);
expect(this.onShowStub.callCount).to.equal(3);
});
it('should call onShow when a single model is added and the collectionView is already shown', function() {
this.collection.add({id: 3});
expect(this.onShowStub.callCount).to.equal(2);
});
});
describe('within a view', function() {
beforeEach(function() {
this.view = new this.View();
this.view.render();
});
it('should not clobber the event prototype', function() {
expect(this.behaviors.foo.prototype.events).to.have.property('click @ui.bar', 'onBarClick');
});
it('should set the behavior UI element', function() {
expect(this.onRenderStub).to.have.been.calledOnce;
});
describe("the $el", function() {
beforeEach(function() {
this.view.$el.find('.foo').click();
this.view.$el.find('.bar').click();
});
it('should handle behavior ui click event', function() {
expect(this.onFooClickStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should handle view ui click event', function() {
expect(this.onBarClickStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
});
describe("the el", function() {
beforeEach(function() {
$(this.view.el).find('.foo').click();
$(this.view.el).find('.bar').click();
});
it('should handle behavior ui click event', function() {
expect(this.onFooClickStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should handle view ui click event', function() {
expect(this.onBarClickStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
});
});
describe('within a layout', function() {
beforeEach(function() {
this.LayoutView = Marionette.LayoutView.extend({
template: _.template('<div class="baz"></div>'),
regions: { bazRegion: '.baz' }
});
this.layoutView = new this.LayoutView();
this.layoutView.render();
this.layoutView.bazRegion.show(new this.View());
this.layoutView.destroy();
});
it('should call onBeforeShow', function() {
expect(this.onBeforeShowStub).to.have.been.calledOnce;
});
it('should call onShow', function() {
expect(this.onShowStub).to.have.been.calledOnce;
});
it('should call onDestroy', function() {
expect(this.onDestroyStub).to.have.been.calledOnce;
});
});
});
describe('showing a view in a layout', function() {
beforeEach(function() {
this.onShowStub = this.sinon.stub();
this.onDestroyStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
onShow : this.onShowStub,
onDestroy : this.onDestroyStub
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.ItemView.extend({
template: _.template('foo'),
behaviors: { foo: {} }
});
this.region = new Backbone.Marionette.Region({ el: $('<div>') });
this.view = new this.View();
this.region.show(this.view);
this.region.empty();
});
it('behavior onShow is called once', function() {
expect(this.onShowStub).to.have.been.calledOnce;
});
it('behavior onClose is called once', function() {
expect(this.onDestroyStub).to.have.been.calledOnce;
});
});
describe('behavior instance events', function() {
beforeEach(function() {
var suite = this;
this.listenToChangeStub = this.sinon.stub();
this.onFooStub = this.sinon.stub();
this.model = new Backbone.Model();
this.FooBehavior = Marionette.Behavior.extend({
initialize: function() {
this.listenTo(suite.model, 'change', suite.listenToChangeStub);
this.on('foo', suite.onFooStub);
}
});
this.View = Marionette.View.extend({
behaviors: { foo: { behaviorClass: this.FooBehavior } }
});
this.view = new this.View();
this.view.destroy();
this.model.set('bar', 'baz');
this.view.triggerMethod('foo');
});
it('should unbind listenTo on destroy', function() {
expect(this.listenToChangeStub).not.to.have.been.calledOnce;
});
it('should still be bound to "on" on destroy', function() {
expect(this.onFooStub).to.have.been.calledOnce;
});
});
describe('behavior model events', function() {
beforeEach(function() {
var suite = this;
this.handleModelChangeStub = this.sinon.stub();
this.handleCollectionResetStub = this.sinon.stub();
this.handleModelFooChangeStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
initialize: function() { suite.fooBehavior = this; },
modelEvents: {
'change': this.handleModelChangeStub,
'change:foo': 'handleModelFooChange'
},
collectionEvents: {
'reset': this.handleCollectionResetStub
},
handleModelFooChange: this.handleModelFooChangeStub
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.CollectionView = Marionette.CollectionView.extend({
behaviors: { foo: {} }
});
this.ItemView = Marionette.ItemView.extend({
behaviors: { foo: {} }
});
this.model = new Backbone.Model({ foo: 'bar' });
this.collection = new Backbone.Collection([]);
});
it('should proxy model events', function() {
this.view = new this.ItemView({ model: this.model });
this.model.set('foo', 'baz');
expect(this.handleModelChangeStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should proxy model events w/ string cbk', function() {
this.view = new this.ItemView({ model: this.model });
this.model.set('foo', 'baz');
expect(this.handleModelFooChangeStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should proxy collection events', function() {
this.view = new this.CollectionView({ collection: this.collection });
this.collection.reset();
expect(this.handleCollectionResetStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should unbind model events on view undelegate', function() {
this.view = new this.ItemView({ model: this.model });
this.view.undelegateEvents();
this.model.set('foo', 'doge');
expect(this.handleModelFooChangeStub).not.to.have.been.called;
});
it('should unbind collection events on view undelegate', function() {
this.view = new this.CollectionView({ collection: this.collection });
this.view.undelegateEvents();
this.collection.reset();
expect(this.handleCollectionResetStub).not.to.have.been.called;
});
});
describe('behavior trigger calls', function() {
beforeEach(function() {
this.onRenderStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
onRender: this.onRenderStub
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.View.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
this.view.triggerMethod('render');
});
it('should call onRender when a view is rendered', function() {
expect(this.onRenderStub).to.have.been.calledOnce;
});
});
describe('behavior triggerMethod calls', function() {
beforeEach(function() {
this.behaviors = {
foo: Marionette.Behavior.extend({
onFoo: function() {
return "behavior foo";
}
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.View.extend({
behaviors: { foo: {} },
onFoo: function() {
return "view foo";
}
});
this.view = new this.View();
});
it('onFoo should return "foo"', function() {
expect(this.view.triggerMethod('foo')).to.equal('view foo');
});
});
describe('behavior is evented', function() {
beforeEach(function() {
this.listenToStub = this.sinon.stub();
this.changeStub = this.sinon.stub();
this.behavior = new Marionette.Behavior({}, {});
this.model = new Backbone.Model();
Marionette.bindEntityEvents(this.behavior, this.model, {
'change': this.changeStub
});
this.behavior.listenTo(this.model, 'foo', this.listenToStub);
this.model.trigger('foo');
this.model.set('foo', 'bar');
});
it('should listenTo events', function() {
expect(this.listenToStub).to.have.been.calledOnce;
});
it('should support bindEntityEvents', function() {
expect(this.changeStub).to.have.been.calledOnce;
});
it('should execute in the specified context', function() {
expect(this.listenToStub).to.have.been.calledOnce.and.calledOn(this.behavior);
});
});
describe('behavior with behavior', function() {
beforeEach(function() {
var suite = this;
this.initializeStub = this.sinon.stub();
this.viewOnRenderStub = this.sinon.stub();
this.fooClickStub = this.sinon.stub();
this.barOnRenderStub = this.sinon.stub();
this.barClickStub = this.sinon.stub();
this.barModelChangeStub = this.sinon.stub();
this.barCollectionSyncStub = this.sinon.stub();
this.viewOnRenderStub = this.sinon.stub();
this.bazClickStub = this.sinon.stub();
this.behaviors = {
foo: Marionette.Behavior.extend({
initialize: function() { suite.fooBehavior = this; },
behaviors: { bar: {} },
ui: { foo: '.foo' },
events: {
'click @ui.foo': this.fooClickStub
}
}),
bar: Marionette.Behavior.extend({
initialize: function() {
suite.initializeStub();
suite.barBehavior = this;
},
onRender: this.barOnRenderStub,
ui: { bar: '.bar' },
events: {
'click @ui.bar': this.barClickStub
},
modelEvents: {
'change': this.barModelChangeStub
},
collectionEvents: {
'sync': this.barCollectionSyncStub
}
})
};
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.CompositeView.extend({
template: _.template('<div class="baz"></div><div class="foo"></div><div class="bar"></div>'),
behaviors: { foo: {} },
onRender: this.viewOnRenderStub,
ui: { baz: '.baz' },
events: {
'click @ui.baz': this.bazClickStub,
}
});
this.model = new Backbone.Model();
this.collection = new Backbone.Collection();
this.view = new this.View({
model : this.model,
collection : this.collection
});
this.sinon.spy(this.view, 'undelegateEvents');
});
it('should call initialize on grouped behaviors', function() {
expect(this.initializeStub).to.have.been.calledOnce;
});
it('should set _behaviors', function() {
expect(this.view._behaviors.length).to.be.equal(2);
});
it('should call onRender on grouped behaviors', function() {
this.view.triggerMethod('render');
expect(this.barOnRenderStub).to.have.been.calledOnce.and.calledOn(this.barBehavior);
});
it('should call onRender on the view', function() {
this.view.triggerMethod('render');
expect(this.viewOnRenderStub).to.have.been.calledOnce.and.calledOn(this.view);
});
it('should call undelegateEvents once', function() {
this.view.undelegateEvents();
expect(this.view.undelegateEvents).to.have.been.calledOnce;
});
it('should proxy modelEvents to grouped behaviors', function() {
this.model.trigger('change');
expect(this.barModelChangeStub).to.have.been.calledOnce.and.calledOn(this.barBehavior);
});
it('should proxy collectionEvents to grouped behaviors', function() {
this.collection.trigger('sync');
expect(this.barCollectionSyncStub).to.have.been.calledOnce.and.calledOn(this.barBehavior);
});
it('should proxy child behavior UI events to grouped behaviors', function() {
this.view.render();
this.barBehavior.ui.bar.click();
expect(this.barClickStub).to.have.been.calledOnce.and.calledOn(this.barBehavior);
});
it('should proxy base behavior UI events to base behavior', function() {
this.view.render();
this.fooBehavior.ui.foo.click();
expect(this.fooClickStub).to.have.been.calledOnce.and.calledOn(this.fooBehavior);
});
it('should proxy view UI events to view', function() {
this.view.render();
this.view.ui.baz.click();
expect(this.bazClickStub).to.have.been.calledOnce.and.calledOn(this.view);
});
});
describe('return values of wrapped methods', function() {
beforeEach(function() {
this.behaviors = { foo: Marionette.Behavior };
Marionette.Behaviors.behaviorsLookup = this.behaviors;
this.View = Marionette.View.extend({
behaviors: { foo: {} }
});
this.view = new this.View();
});
it('destroy should return the view', function() {
this.sinon.spy(this.view, 'destroy');
this.view.destroy();
expect(this.view.destroy).to.have.returned(this.view);
});
it('setElement should return the view', function() {
this.sinon.spy(this.view, 'setElement');
this.view.setElement(this.view.$el);
expect(this.view.setElement).to.have.returned(this.view);
});
it('delegateEvents should return the view', function() {
this.sinon.spy(this.view, 'delegateEvents');
this.view.delegateEvents();
expect(this.view.delegateEvents).to.have.returned(this.view);
});
it('undelegateEvents should return the view', function() {
this.sinon.spy(this.view, 'undelegateEvents');
this.view.undelegateEvents({});
expect(this.view.undelegateEvents).to.have.returned(this.view);
});
});
describe('.destroy', function() {
beforeEach(function() {
this.behavior = new Marionette.Behavior();
this.sinon.spy(this.behavior, 'destroy');
this.behavior.destroy();
});
it('should return the behavior', function() {
});
});
});