/test/unit/composite-view.spec.js
JavaScript | 839 lines | 655 code | 169 blank | 15 comment | 7 complexity | 121f0f1472de69f92f60ddf53d527690 MD5 | raw file
- describe('composite view', function() {
- 'use strict';
- beforeEach(function() {
- var suite = this;
- // Models
- this.Model = Backbone.Model.extend();
- this.User = Backbone.Model.extend();
- this.Node = Backbone.Model.extend({
- initialize: function() {
- var nodes = this.get('nodes');
- if (nodes) {
- this.nodes = new suite.NodeCollection(nodes);
- this.unset('nodes');
- }
- }
- });
- // Collections
- this.Collection = Backbone.Collection.extend({
- model: this.Model
- });
- this.UserCollection = Backbone.Collection.extend({
- model: this.User
- });
- this.NodeCollection = Backbone.Collection.extend({
- model: this.Node
- });
- // Views
- this.treeViewTemplateFn = _.template('<li>name: <%= name %></li>');
- this.TreeView = Backbone.Marionette.CompositeView.extend({
- tagName: 'ul',
- template: this.treeViewTemplateFn,
- initialize: function() {
- this.collection = this.model.nodes;
- }
- });
- });
- describe('when a composite view has a template without a model', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite template');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeViewNoModel = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection([this.m1, this.m2]);
- this.compositeView = new this.CompositeViewNoModel({
- collection: this.collection
- });
- this.compositeView.render();
- });
- it('should render the template', function() {
- expect(this.compositeView.$el).to.contain.$text('composite');
- });
- it('should render the collections items', function() {
- expect(this.compositeView.$el).to.contain.$text('bar');
- expect(this.compositeView.$el).to.contain.$text('baz');
- });
- });
- describe('when rendering with a overridden attachElContent', function() {
- beforeEach(function() {
- this.attachElContentStub = this.sinon.stub();
- this.CompositeView = Marionette.CompositeView.extend({
- template: function() {},
- attachElContent: this.attachElContentStub
- });
- this.compositeView = new this.CompositeView();
- this.compositeView.render();
- });
- it('should render according to the custom attachElContent logic', function() {
- expect(this.attachElContentStub).to.have.been.calledOnce.and.calledWith(undefined);
- });
- });
- describe('when a composite view has a model and a template', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn,
- onRender: function() {}
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection();
- this.collection.add(this.m2);
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: this.collection
- });
- this.sinon.spy(Marionette.Renderer, 'render');
- this.compositeView.render();
- });
- it('should render the template with the model', function() {
- expect(this.compositeView.$el).to.contain.$text('composite bar');
- });
- it('should render the collections items', function() {
- expect(this.compositeView.$el).to.contain.$text('baz');
- });
- it('should pass template fn, data, and view instance to Marionette.Renderer.Render', function() {
- expect(Marionette.Renderer.render).to.have.been.calledWith(this.templateFn, {foo: 'bar'}, this.compositeView);
- });
- });
- describe('when a composite view triggers render in initialize', function() {
- beforeEach(function() {
- var suite = this;
- this.collectionTemplateFn = _.template('');
- this.collectionItemTemplateFn = _.template('<% _.each(items, function(item){ %><span><%= item.foo %></span><% }) %>');
- this.emptyTemplateFn = _.template(' ');
- this.EmptyView = Backbone.Marionette.ItemView.extend({
- template: this.emptyTemplateFn,
- tagName: 'hr',
- onShow: function() {
- suite.onShow.push('EMPTY');
- }
- });
- this.ChildView = Backbone.Marionette.ItemView.extend({
- template: this.collectionItemTemplateFn,
- tagName: 'span'
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- emptyView: this.EmptyView,
- template: this.collectionTemplateFn,
- initialize: function() {
- this.render();
- },
- onRender: function() {}
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: new this.Collection()
- });
- this.onShow = [];
- this.compositeView.trigger('show');
- });
- it('should call "onShowCallbacks.add"', function() {
- expect(this.onShow.length === 1).to.be.ok;
- });
- });
- describe('when rendering a composite view without a template', function() {
- beforeEach(function() {
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection();
- this.collection.add(this.m2);
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: this.collection
- });
- });
- it('should throw an exception because there was no valid template', function() {
- expect(this.compositeView.render).to.throw('Cannot render the template since its false, null or undefined.');
- });
- });
- describe('when rendering a composite view', function() {
- beforeEach(function() {
- var suite = this;
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn,
- onBeforeRender: function() {
- return this.isRendered;
- },
- onRender: function() {
- return this.isRendered;
- }
- });
- this.order = [];
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection();
- this.collection.add(this.m2);
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: this.collection
- });
- this.compositeView.on('render:template', function() {
- suite.order.push(suite.compositeView.renderedModelView);
- });
- this.compositeView.on('render:collection', function() {
- suite.order.push(suite.compositeView.collection);
- });
- this.compositeView.on('render', function() {
- suite.order.push(suite.compositeView);
- });
- this.sinon.spy(this.compositeView, 'trigger');
- this.sinon.spy(this.compositeView, 'onBeforeRender');
- this.sinon.spy(this.compositeView, 'onRender');
- this.compositeView.render();
- });
- it('should trigger a render event for the model view', function() {
- expect(this.compositeView.trigger).to.have.been.calledWith('render:template');
- });
- it('should trigger a before:render event for the collection', function() {
- expect(this.compositeView.trigger).to.have.been.calledWith('before:render:collection', this.compositeView);
- });
- it('should trigger a render event for the collection', function() {
- expect(this.compositeView.trigger).to.have.been.calledWith('render:collection', this.compositeView);
- });
- it('should trigger a render event for the composite view', function() {
- expect(this.compositeView.trigger).to.have.been.calledWith('render', this.compositeView);
- });
- it('should guarantee rendering of the model before rendering the collection', function() {
- expect(this.order[0]).to.equal(this.compositeView.renderedModelView);
- expect(this.order[1]).to.equal(this.compositeView.collection);
- expect(this.order[2]).to.equal(this.compositeView);
- });
- it('should call "onBeforeRender"', function() {
- expect(this.compositeView.onBeforeRender).to.have.been.calledOnce;
- });
- it('should call "onRender"', function() {
- expect(this.compositeView.onRender).to.have.been.calledOnce;
- });
- it('should call "onBeforeRender" before "onRender"', function() {
- expect(this.compositeView.onBeforeRender).to.have.been.calledBefore(this.compositeView.onRender);
- });
- it('should not be rendered when "onBeforeRender" is called', function() {
- expect(this.compositeView.onBeforeRender.lastCall.returnValue).not.to.be.ok;
- });
- it('should be rendered when "onRender" is called', function() {
- expect(this.compositeView.onRender.lastCall.returnValue).to.be.true;
- });
- it('should mark as rendered', function() {
- expect(this.compositeView).to.have.property('isRendered', true);
- });
- });
- describe('when rendering a composite view twice', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeModelView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection();
- this.collection.add(this.m2);
- this.compositeView = new this.CompositeModelView({
- model: this.m1,
- collection: this.collection
- });
- this.sinon.spy(this.compositeView, 'render');
- this.sinon.spy(this.compositeView, 'destroyChildren');
- this.sinon.spy(Backbone.Marionette.Renderer, 'render');
- this.compositeRenderSpy = this.compositeView.render;
- this.compositeView.render();
- this.compositeView.render();
- });
- it('should re-render the template view', function() {
- expect(Backbone.Marionette.Renderer.render.callCount).to.equal(2);
- });
- it('should destroy all of the child collection child views', function() {
- expect(this.compositeView.destroyChildren).to.have.been.called;
- expect(this.compositeView.destroyChildren.callCount).to.equal(2);
- });
- it('should re-render the collections items', function() {
- expect(this.compositeRenderSpy.callCount).to.equal(2);
- });
- });
- describe('when rendering a composite view with an empty collection and then resetting the collection', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn,
- onRender: function() {}
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.collection = new this.Collection();
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: this.collection
- });
- this.compositeView.render();
- this.m2 = new this.Model({foo: 'baz'});
- this.collection.reset([this.m2]);
- });
- it('should render the template with the model', function() {
- expect(this.compositeView.$el).to.contain.$text('composite bar');
- });
- it('should render the collections items', function() {
- expect(this.compositeView.$el).to.contain.$text('baz');
- });
- });
- describe('when rendering a composite view without a collection', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn,
- onRender: function() {}
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.compositeView = new this.CompositeView({
- model: this.m1
- });
- this.compositeView.render();
- });
- it('should render the template with the model', function() {
- expect(this.compositeView.$el).to.contain.$text('composite bar');
- });
- it('should not render the collections items', function() {
- expect(this.compositeView.$el).not.to.contain.$text('baz');
- });
- });
- describe('when rendering a composite with a collection', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.childViewTagName = 'span';
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: this.childViewTagName,
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn,
- onRender: function() {}
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection([this.m2]);
- this.compositeView = new this.CompositeView({
- model: this.m1,
- collection: this.collection
- });
- this.compositeView.render();
- this.sinon.spy(this.compositeView, '_renderTemplate');
- this.sinon.spy(this.compositeView, 'getChildViewContainer');
- });
- describe('and then resetting the collection', function() {
- beforeEach(function() {
- this.m3 = new this.Model({foo: 'quux'});
- this.m4 = new this.Model({foo: 'widget'});
- this.collection.reset([this.m3, this.m4]);
- });
- it('should not re-render the template with the model', function() {
- expect(this.compositeView._renderTemplate).not.to.have.been.called;
- });
- it('should render the collections items', function() {
- expect(this.compositeView.$el).not.to.contain.$text('baz');
- expect(this.compositeView.$el).to.contain.$text('quux');
- expect(this.compositeView.$el).to.contain.$text('widget');
- });
- });
- describe('and then adding to the collection', function() {
- beforeEach(function() {
- this.m3 = new this.Model({foo: 'quux'});
- this.collection.add(this.m3);
- });
- it('should not re-render the template with the model', function() {
- expect(this.compositeView._renderTemplate).not.to.have.been.called;
- });
- it('should add to the collections items', function() {
- expect(this.compositeView.$el).to.contain.$text('bar');
- expect(this.compositeView.$el).to.contain.$text('baz');
- expect(this.compositeView.$el).to.contain.$text('quux');
- });
- it('shound send childView to getChildViewContainer', function() {
- expect(this.compositeView.getChildViewContainer).to.have.been.called;
- expect(this.compositeView.getChildViewContainer.getCall(0).args[1].tagName).to.equal(this.childViewTagName);
- });
- });
- describe('and then removing from the collection', function() {
- beforeEach(function() {
- this.model = this.collection.at(0);
- this.collection.remove(this.model);
- });
- it('should not re-render the template with the model', function() {
- expect(this.compositeView._renderTemplate).not.to.have.been.called;
- });
- it('should remove from the collections items', function() {
- expect(this.compositeView.$el).not.to.contain.$text('baz');
- });
- });
- });
- describe('when working with a composite and recursive model', function() {
- beforeEach(function() {
- this.data = {
- name: 'level 1',
- nodes: [
- {
- name: 'level 2',
- nodes: [
- {
- name: 'level 3'
- }
- ]
- }
- ]
- };
- this.node = new this.Node(this.data);
- this.treeView = new this.TreeView({
- model: this.node
- });
- this.treeView.render();
- });
- it('should render the template with the model', function() {
- expect(this.treeView.$el).to.contain.$text('level 1');
- });
- it('should render the collections items', function() {
- expect(this.treeView.$el).to.contain.$text('level 2');
- });
- it('should render all the levels of the nested object', function() {
- expect(this.treeView.$el).to.contain.$text('level 3');
- });
- });
- describe('when destroying a composite view', function() {
- beforeEach(function() {
- this.templateFn = _.template('composite <%= foo %>');
- this.ChildView = Backbone.Marionette.ItemView.extend({
- tagName: 'span',
- render: function() {
- this.$el.html(this.model.get('foo'));
- }
- });
- this.CompositeModelView = Backbone.Marionette.CompositeView.extend({
- childView: this.ChildView,
- template: this.templateFn
- });
- this.m1 = new this.Model({foo: 'bar'});
- this.m2 = new this.Model({foo: 'baz'});
- this.collection = new this.Collection();
- this.collection.add(this.m2);
- this.compositeView = new this.CompositeModelView({
- model: this.m1,
- collection: this.collection
- });
- this.sinon.spy(this.CompositeModelView.prototype, 'destroy');
- this.compositeView.render();
- this.compositeView.destroy();
- });
- it('should delete the model view', function() {
- expect(this.compositeView.renderedModelView).to.be.undefined;
- });
- it('should destroy the collection of views', function() {
- expect(this.CompositeModelView.prototype.destroy).to.have.been.calledOnce;
- });
- it('should be marked destroyed', function() {
- expect(this.compositeView).to.have.property('isDestroyed', true);
- });
- it('should be marked not rendered', function() {
- expect(this.compositeView).to.have.property('isRendered', false);
- });
- });
- describe('when rendering a composite view with no model, using a template to create a grid', function() {
- beforeEach(function() {
- this.gridTemplateFn = _.template('<thead><tr><th>Username</th><th>Full Name</th><tr></thead><tbody></tbody>');
- this.gridRowTemplateFn = _.template('<td><%= username %></td><td><%= fullname %></td>');
- // A Grid Row
- this.GridRow = Backbone.Marionette.ItemView.extend({
- tagName: 'tr',
- template: this.gridRowTemplateFn
- });
- // The grid view
- this.GridView = Backbone.Marionette.CompositeView.extend({
- tagName: 'table',
- template: this.gridTemplateFn,
- childView: this.GridRow,
- attachHtml: function(collectionView, itemView) {
- collectionView.$('tbody').append(itemView.el);
- }
- });
- this.userData = [
- {
- username: 'dbailey',
- fullname: 'Derick Bailey'
- },
- {
- username: 'jbob',
- fullname: 'Joe Bob'
- },
- {
- username: 'fbar',
- fullname: 'Foo Bar'
- }
- ];
- this.userList = new this.UserCollection(this.userData);
- this.gridView = new this.GridView({
- tagName: 'table',
- collection: this.userList
- });
- this.gridView.render();
- });
- it('should render the table', function() {
- expect(this.gridView.$('th').length).not.to.equal(0);
- });
- it('should render the users', function() {
- var body = this.gridView.$('tbody');
- expect(body).to.contain.$text('dbailey');
- expect(body).to.contain.$text('jbob');
- expect(body).to.contain.$text('fbar');
- });
- });
- describe('when a composite view has a ui elements hash', function() {
- beforeEach(function() {
- this.gridTemplateFn = _.template('<thead><tr><th>Username</th><th>Full Name</th><tr></thead><tbody></tbody>');
- this.gridRowTemplateFn = _.template('<td><%= username %></td><td><%= fullname %></td>');
- this.GridViewWithUIBindingsTemplateFn = _.template('<thead><tr><th><%= userHeader %></th><th><%= nameHeader %></th><tr></thead><tbody></tbody>');
- // A Grid Row
- this.GridRow = Backbone.Marionette.ItemView.extend({
- tagName: 'tr',
- template: this.gridRowTemplateFn
- });
- // The grid view
- this.GridView = Backbone.Marionette.CompositeView.extend({
- tagName: 'table',
- template: this.gridTemplateFn,
- childView: this.GridRow,
- attachHtml: function(collectionView, itemView) {
- collectionView.$('tbody').append(itemView.el);
- }
- });
- this.GridViewWithUIBindings = this.GridView.extend({
- template: this.GridViewWithUIBindingsTemplateFn,
- ui: {
- headersRow: 'thead tr',
- unfoundElement: '#unfound',
- itemRows: 'tbody tr'
- }
- });
- this.userData = [
- {
- username: 'dbailey',
- fullname: 'Derick Bailey'
- },
- {
- username: 'jbob',
- fullname: 'Joe Bob'
- }
- ];
- this.headersModel = new Backbone.Model({
- userHeader: 'Username',
- nameHeader: 'Full name'
- });
- this.userList = new this.UserCollection(this.userData);
- this.gridView = new this.GridViewWithUIBindings({
- tagName: 'table',
- model: this.headersModel,
- collection: this.userList
- });
- // We don't render the view here since we need more fine-tuned control on when the view is rendered,
- // specifically in the test that asserts the composite view template elements are accessible before
- // the collection is rendered.
- });
- describe('after the whole composite view finished rendering', function() {
- beforeEach(function() {
- this.gridView.render();
- });
- describe('accessing a ui element that belongs to the model template', function() {
- it('should return its jQuery selector if it can be found', function() {
- expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('Username');
- });
- it('should return an empty jQuery object if it cannot be found', function() {
- expect(this.gridView.ui.unfoundElement.length).to.equal(0);
- });
- it('should return an up-to-date selector on subsequent renders', function() {
- // asserting state before subsequent render
- expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('Username');
- this.headersModel.set('userHeader', 'User');
- this.gridView.render();
- expect(this.gridView.ui.headersRow.find('th:first-child')).to.contain.$text('User');
- });
- });
- describe('accessing a ui element that belongs to the collection', function() {
- // This test makes it clear that not allowing access to the collection elements is a design decision
- // and not a bug.
- it('should return an empty jQuery object', function() {
- expect(this.gridView.ui.itemRows.length).to.equal(0);
- });
- });
- });
- describe('after the model finished rendering, but before the collection rendered', function() {
- describe('accessing a ui element that belongs to the model template', function() {
- beforeEach(function() {
- var suite = this;
- this.gridView.onBeforeRender = function() {
- suite.called = true;
- };
- this.sinon.spy(this.gridView, 'onBeforeRender');
- this.gridView.render();
- });
- // this test enforces that ui elements should be accessible as soon as their html was inserted
- // to the DOM
- it('should return its jQuery selector', function() {
- expect(this.gridView.onBeforeRender).to.have.been.called;
- });
- it('should set the username', function() {
- expect($(this.gridView.ui.headersRow).find('th:first-child').text()).to.equal('Username');
- });
- });
- });
- });
- describe('when serializing view data', function() {
- beforeEach(function() {
- this.modelData = {foo: 'bar'};
- this.view = new Marionette.CompositeView();
- this.sinon.spy(this.view, 'serializeModel');
- });
- it('should return an empty object without data', function() {
- expect(this.view.serializeData()).to.deep.equal({});
- });
- describe('and the view has a model', function() {
- beforeEach(function() {
- this.view.model = new Backbone.Model(this.modelData);
- this.view.serializeData();
- });
- it('should call serializeModel', function() {
- expect(this.view.serializeModel).to.have.been.calledOnce;
- });
- });
- });
- describe('has a valid inheritance chain back to Marionette.CollectionView', function() {
- beforeEach(function() {
- this.constructor = this.sinon.spy(Marionette, 'CollectionView');
- this.compositeView = new Marionette.CompositeView();
- });
- it('calls the parent Marionette.CollectionViews constructor function on instantiation', function() {
- expect(this.constructor).to.have.been.calledOnce;
- });
- });
- });