UNPKG

backbone.select

Version:

Selecting Backbone models; handling selections in Backbone collections.

709 lines (522 loc) 33 kB
describe( 'Automatic creation of Select.Me models', function () { var /** @type {AutoCreationFixture} */ f, SelectMeModel = Backbone.Model.extend( { tellType: "SelectMeModel", initialize: function ( attributes, options ) { Backbone.Select.Me.applyTo( this, options ); } } ), SelectOneCollection = Backbone.Collection.extend( { initialize: function ( models, options ) { Backbone.Select.One.applyTo( this, models, options ); } } ), SelectManyCollection = Backbone.Collection.extend( { initialize: function ( models, options ) { Backbone.Select.Many.applyTo( this, models, options ); } } ), collectionTypeScenarios = { "Select.One collection": function ( fixture ) { fixture.setBaseCollectionType( SelectOneCollection ); }, "Select.Many collection": function ( fixture ) { fixture.Collection = SelectManyCollection; } }, eventedPopulationScenarios = { "Populating the collection during instantiation": pickTests( { optOut: "singularItemTests", optIn: "defaultLabelTest" }, function ( fixture ) { fixture.creationMethod = "new"; } ), "Populating the collection with add": pickTests( "default", function ( fixture ) { fixture.creationMethod = "add"; } ), "Populating the collection with reset": pickTests( { optOut: "singularItemTests" }, function ( fixture ) { fixture.creationMethod = "reset"; } ), "Populating the collection with set": pickTests( "default", function ( fixture ) { fixture.creationMethod = "set"; } ) }, silentPopulationScenarios = { "Populating the collection with add, with options.silent enabled": pickTests( "default", function ( fixture ) { fixture.creationMethod = "add"; fixture.options.silent = true; } ), "Populating the collection with reset, with options.silent enabled": pickTests( { optOut: "singularItemTests" }, function ( fixture ) { fixture.creationMethod = "reset"; fixture.options.silent = true; } ), "Populating the collection with set, with options.silent enabled": pickTests( "default", function ( fixture ) { fixture.creationMethod = "set"; fixture.options.silent = true; } ) }, populationScenarios = _.extend( {}, eventedPopulationScenarios, silentPopulationScenarios ), parseScenarios = { "models are created from an attributes hash": function ( fixture ) { fixture.modelDataSets = fixture.attributeSets; fixture.firstModelDataSet = fixture.attributeSets[0]; }, "models are created from parsed input, with options.parse set (model data sets stored in an array)": function ( fixture ) { fixture.options.parse = true; fixture.Collection = fixture.Collection.extend( { parse: function ( modelData ) { return _.isArray( modelData ) ? _.pluck( modelData, "nested" ) : [modelData.nested]; } } ); fixture.modelDataSets = _.map( fixture.attributeSets, function ( attributeSet ) { return { nested: attributeSet }; } ); fixture.firstModelDataSet = fixture.modelDataSets[0]; }, "models are created from parsed input, with options.parse set (model data sets wrapped in single outer object)": function ( fixture ) { fixture.options.parse = true; fixture.Collection = fixture.Collection.extend( { parse: function ( modelDataObject ) { return _.pluck( modelDataObject.modelDataStore, "nested" ); } } ); fixture.modelDataSets = { modelDataStore: _.map( fixture.attributeSets, function ( attributeSet ) { return { nested: attributeSet }; } ) }; fixture.firstModelDataSet = { modelDataStore: [ fixture.modelDataSets.modelDataStore[0] ] }; } }, modelTemplateScenarios = { "collection.model is not set, models are created from Backbone.Model": function ( fixture ) { fixture.modelTemplate = Backbone.Model; }, "collection.model is set to a custom type, which does not have the Select.Me mixin applied": function ( fixture ) { fixture.modelTemplate = Backbone.Model.extend( { tellType: "custom" } ); fixture.Collection = fixture.Collection.extend( { model: fixture.modelTemplate } ); }, "collection.model is set to a custom type, which has the Select.Me mixin applied": function ( fixture ) { fixture.modelTemplate = SelectMeModel; fixture.Collection = fixture.Collection.extend( { model: SelectMeModel } ); } }; beforeAll( function () { limitJasmineRecursiveScreenOutput(); } ); afterAll( function () { restoreJasmineRecursiveScreenOutput(); } ); describe( 'Creating models on the fly, from raw data', function () { beforeEach( function () { f = new AutoCreationFixture( [ { number: 1 }, { number: 2 }, { number: 3 } ] ); } ); describeWithData( collectionTypeScenarios, function ( configureCollectionType ) { beforeEach( function () { configureCollectionType( f ); } ); describeWithData( populationScenarios, function ( configurePopulation ) { beforeEach( function () { configurePopulation( f ); } ); describeWithData( parseScenarios, function ( configureParsing ) { beforeEach( function () { configureParsing( f ); } ); describeWithData( modelTemplateScenarios, function ( configureModelTemplate ) { beforeEach( function () { configureModelTemplate( f ); } ); if( !hasOptedOut( configurePopulation, "singularItemTests" ) ) { describe( 'data for single item, not wrapped in an array, is passed in', function () { var modelDataSet, collection, model; beforeEach( function () { modelDataSet = f.firstModelDataSet; collection = f.createPopulatedCollection( modelDataSet ); model = collection.at( 0 ); } ); afterEach( function () { collection.close(); } ); it( 'the collection has the expected number of models', function () { expect( collection.length ).toEqual( 1 ); } ); it( 'the model is of the expected type', function () { expect( model ).toEqual( jasmine.any( f.modelTemplate ) ); } ); it( 'the model has the expected attributes', function () { expect( model.attributes ).toEqual( f.attributeSets[0] ); expect( model.get( "number" ) ).toEqual( 1 ); } ); it( 'the model has the Select.Me mixin applied', function () { expect( model._pickyType ).toEqual( "Backbone.Select.Me" ); } ); if ( hasOptedIn( configurePopulation, "defaultLabelTest" ) ) { it( 'the model shares the defaultLabel setting of the collection', function () { // Only happens when creating the collection. Then, collection and models are // created in a single process, and the options passed to the collection are // used for both. collection.close(); collection = f.createPopulatedCollection( modelDataSet, { defaultLabel: "foo" } ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } } ); } describe( 'an array of item data is passed in', function () { var collection, models; beforeEach( function () { collection = f.createPopulatedCollection( f.modelDataSets ); models = collection.models; } ); afterEach( function () { collection.close(); } ); it( 'the collection has the expected number of models', function () { expect( collection.length ).toEqual( 3 ); } ); it( 'the models are of the expected type', function () { expect( models[0] ).toEqual( jasmine.any( f.modelTemplate ) ); expect( models[1] ).toEqual( jasmine.any( f.modelTemplate ) ); expect( models[2] ).toEqual( jasmine.any( f.modelTemplate ) ); } ); it( 'the models have the expected attributes', function () { expect( models[0].attributes ).toEqual( f.attributeSets[0] ); expect( models[1].attributes ).toEqual( f.attributeSets[1] ); expect( models[2].attributes ).toEqual( f.attributeSets[2] ); expect( models[0].get( "number" ) ).toEqual( 1 ); expect( models[1].get( "number" ) ).toEqual( 2 ); expect( models[2].get( "number" ) ).toEqual( 3 ); } ); it( 'the models have the Select.Me mixin applied', function () { expect( models[0]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[1]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[2]._pickyType ).toEqual( "Backbone.Select.Me" ); } ); if ( hasOptedIn( configurePopulation, "defaultLabelTest" ) ) { it( 'the models share the defaultLabel setting of the collection', function () { // Only happens when creating the collection. Then, collection and models are // created in a single process, and the options passed to the collection are // used for both. collection.close(); collection = f.createPopulatedCollection( f.modelDataSets, { defaultLabel: "foo" } ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 1 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 2 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } } ); } ); } ); } ); } ); } ); describe( 'Auto-Applying the Select.Me mixin on the fly, to Backbone models', function () { beforeEach( function () { f = new AutoCreationFixture( [ { number: 1 }, { number: 2 }, { number: 3 } ] ); } ); describeWithData( collectionTypeScenarios, function ( configureCollectionType ) { beforeEach( function () { configureCollectionType( f ); } ); describeWithData( populationScenarios, function ( configurePopulation ) { beforeEach( function () { configurePopulation( f ); } ); if( !hasOptedOut( configurePopulation, "singularItemTests" ) ) { describe( 'data for single item, not wrapped in an array, is passed in', function () { var collection, model; beforeEach( function () { var inputModel = f.createFirstPlainModel(); collection = f.createPopulatedCollection( inputModel ); model = collection.at( 0 ); } ); afterEach( function () { collection.close(); } ); it( 'the collection has the expected number of models', function () { expect( collection.length ).toEqual( 1 ); } ); it( 'the model is of the expected type', function () { expect( model ).toEqual( jasmine.any( Backbone.Model ) ); } ); it( 'the model has the expected attributes', function () { expect( model.attributes ).toEqual( f.attributeSets[0] ); expect( model.get( "number" ) ).toEqual( 1 ); } ); it( 'the model has the Select.Me mixin applied', function () { expect( model._pickyType ).toEqual( "Backbone.Select.Me" ); } ); it( 'the model is selectable immediately after the collection mixin has been applied', function () { var inputModel, firstSelectedItemInCollection, originalInitialize = collection.initialize; f.Collection = f.Collection.extend( { initialize: function ( models, options ) { originalInitialize.apply( this, arguments ); // When testing with models which are passed in during instantiation if ( models ) models[0].select(); // When testing with models which are passed in during add, set or reset this.listenTo( this, "add", function () { this.at(0).select() } ); this.listenTo( this, "set", function () { this.at(0).select() } ); this.listenTo( this, "reset", function () { this.at(0).select() } ); } } ); collection.close(); inputModel = f.createFirstPlainModel(); collection = f.createPopulatedCollection( inputModel ); if ( f.options.silent ) { // With options silent, we haven't been able to trigger an automatic selection after // "add"/"set"/"reset". We do it manually here, so see if things work, even though it is // stretching the concept of "immediately after the mixin is applied". collection.at( 0 ).select(); } firstSelectedItemInCollection = collection._pickyType === "Backbone.Select.One" ? collection.selected : _.values( collection.selected )[0]; expect( inputModel.selected ).toBe( true ); expect( firstSelectedItemInCollection ).toBe( inputModel ); } ); if ( hasOptedIn( configurePopulation, "defaultLabelTest" ) ) { it( 'the model shares the defaultLabel setting of the collection', function () { // Only happens when creating the collection. Then, collection and models are // created in a single process, and the options passed to the collection are // used for both. collection.close(); collection = f.createPopulatedCollection( f.createFirstPlainModel(), { defaultLabel: "foo" } ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } it( 'options are passed on to the the Select.Me mixin as it is applied', function () { // Testing with the defaultLabel option, the only option recognized by Select.Me.applyTo(). _.extend( f.options, { defaultLabel: "foo" } ); collection.close(); collection = f.createPopulatedCollection( f.createFirstPlainModel() ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } ); } describe( 'an array of item data is passed in', function () { var collection, models; beforeEach( function () { collection = f.createPopulatedCollection( f.createPlainModels() ); models = collection.models; } ); afterEach( function () { collection.close(); } ); it( 'the collection has the expected number of models', function () { expect( collection.length ).toEqual( 3 ); } ); it( 'the models are of the expected type', function () { expect( models[0] ).toEqual( jasmine.any( Backbone.Model ) ); expect( models[1] ).toEqual( jasmine.any( Backbone.Model ) ); expect( models[2] ).toEqual( jasmine.any( Backbone.Model ) ); } ); it( 'the models have the expected attributes', function () { expect( models[0].attributes ).toEqual( f.attributeSets[0] ); expect( models[1].attributes ).toEqual( f.attributeSets[1] ); expect( models[2].attributes ).toEqual( f.attributeSets[2] ); expect( models[0].get( "number" ) ).toEqual( 1 ); expect( models[1].get( "number" ) ).toEqual( 2 ); expect( models[2].get( "number" ) ).toEqual( 3 ); } ); it( 'the models are selectable immediately after the collection mixin has been applied', function () { var firstSelectedItemInCollection, originalInitialize = collection.initialize, inputModels = f.createPlainModels(); f.Collection = f.Collection.extend( { initialize: function ( models, options ) { originalInitialize.apply( this, arguments ); // When testing with models which are passed in during instantiation if ( models ) models[0].select(); // When testing with models which are passed in during add, set or reset this.listenTo( this, "add", function () { this.at(0).select() } ); this.listenTo( this, "set", function () { this.at(0).select() } ); this.listenTo( this, "reset", function () { this.at(0).select() } ); } } ); collection.close(); collection = f.createPopulatedCollection( inputModels ); if ( f.options.silent ) { // With options silent, we haven't been able to trigger an automatic selection after // "add"/"set"/"reset". We do it manually here, so see if things work, even though it is // stretching the concept of "immediately after the mixin is applied". collection.at( 0 ).select(); } firstSelectedItemInCollection = collection._pickyType === "Backbone.Select.One" ? collection.selected : _.values( collection.selected )[0]; expect( inputModels[0].selected ).toBe( true ); expect( firstSelectedItemInCollection ).toBe( inputModels[0] ); } ); it( 'the models have the Select.Me mixin applied', function () { expect( models[0]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[1]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[2]._pickyType ).toEqual( "Backbone.Select.Me" ); } ); if ( hasOptedIn( configurePopulation, "defaultLabelTest" ) ) { it( 'the models share the defaultLabel setting of the collection', function () { // Only happens when creating the collection. Then, collection and models are // created in a single process, and the options passed to the collection are // used for both. collection.close(); collection = f.createPopulatedCollection( f.createPlainModels(), { defaultLabel: "foo" } ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 1 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 2 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } it( 'options are passed on to the the Select.Me mixin as it is applied', function () { // Testing with the defaultLabel option, the only option recognized by Select.Me.applyTo(). _.extend( f.options, { defaultLabel: "foo" } ); collection.close(); collection = f.createPopulatedCollection( f.createPlainModels() ); expect( collection.at( 0 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 1 )._pickyDefaultLabel ).toEqual( "foo" ); expect( collection.at( 2 )._pickyDefaultLabel ).toEqual( "foo" ); } ); } ); } ); } ); } ); describe( 'Creating models on the fly, special case: one item is undefined, in an array of otherwise valid data', function () { beforeEach( function () { f = new AutoCreationFixture( [ { number: 1 }, undefined, { number: 3 } ] ); } ); describeWithData( collectionTypeScenarios, function ( configureCollectionType ) { beforeEach( function () { configureCollectionType( f ); } ); describeWithData( eventedPopulationScenarios, function ( configurePopulation ) { beforeEach( function () { configurePopulation( f ); } ); describeWithData( modelTemplateScenarios, function ( configureModelTemplate ) { var collection, models; beforeEach( function () { configureModelTemplate( f ); collection = f.createPopulatedCollection( f.attributeSets ); models = collection.models; } ); afterEach( function () { collection.close(); } ); it( 'the collection has the expected number of models, including one for the undefined data', function () { expect( collection.length ).toEqual( 3 ); expect( collection.models.length ).toEqual( 3 ); } ); it( 'the entry corresponding to the undefined input data has been turned into a model without attributes', function () { expect( models[1] ).toEqual( jasmine.any( f.modelTemplate ) ); expect( models[1].attributes ).toEqual( {} ); } ); it( 'the other models are of the expected type', function () { expect( models[0] ).toEqual( jasmine.any( f.modelTemplate ) ); expect( models[2] ).toEqual( jasmine.any( f.modelTemplate ) ); } ); it( 'the other models have the expected attributes', function () { expect( models[0].attributes ).toEqual( f.attributeSets[0] ); expect( models[2].attributes ).toEqual( f.attributeSets[2] ); expect( models[0].get( "number" ) ).toEqual( 1 ); expect( models[2].get( "number" ) ).toEqual( 3 ); } ); it( 'all models, including the one created from undefined data, have the Select.Me mixin applied', function () { expect( models[0]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[1]._pickyType ).toEqual( "Backbone.Select.Me" ); expect( models[2]._pickyType ).toEqual( "Backbone.Select.Me" ); } ); } ); } ); } ); } ); describe( 'Backbone.Select.Me.custom.applyModelMixin is defined', function () { var origApplyModelMixin, modelCreationScenarios = _.extend( parseScenarios, { "existing models are passed in, without the Select.Me mixin being applied": function ( fixture ) { fixture.modelDataSets = _.map( fixture.attributeSets, function ( attributeSet ) { return new Backbone.Model( attributeSet ); } ); } } ); beforeEach( function () { f = new AutoCreationFixture( [ { number: 1 }, { number: 2 }, { number: 3 } ] ); origApplyModelMixin = Backbone.Select.Me.custom.applyModelMixin; Backbone.Select.Me.custom.applyModelMixin = function ( model, collection, options ) { Backbone.Select.Me.applyTo( model, options ); model._detectedPickyType = collection._pickyType; }; } ); afterEach( function () { Backbone.Select.Me.custom.applyModelMixin = origApplyModelMixin; } ); describeWithData( collectionTypeScenarios, function ( configureCollectionType ) { beforeEach( function () { configureCollectionType( f ); } ); describeWithData( modelCreationScenarios, function ( configureParsing ) { beforeEach( function () { configureParsing( f ); } ); describe( 'During instantiation', function () { var collection, models; beforeEach( function () { f.creationMethod = "new"; spyOn( Backbone.Select.Me.custom, "applyModelMixin" ).and.callThrough(); collection = f.createPopulatedCollection( f.modelDataSets ); models = collection.models; } ); afterEach( function () { collection.close(); } ); it( 'applyModelMixin() is called once per model', function () { expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveCallCount( 3 ); } ); it( 'applyModelMixin() is called with the model, not yet augmented with the mixin, as the first argument', function () { expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[0] ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[1] ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[2] ); } ); it( 'applyModelMixin() is called with the collection as the second argument', function () { expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[0], collection ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[1], collection ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[2], collection ); } ); it( 'applyModelMixin() is called with the options, which would otherwise be passed to Backbone.Select.Me.applyTo(), as the third argument', function () { var options = f.options.parse ? jasmine.objectContaining( { parse: true } ) : jasmine.any( Object ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[0], collection, options ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[0], collection, options ); expect( Backbone.Select.Me.custom.applyModelMixin ).toHaveBeenCalledWithInitial( models[0], collection, options ); } ); it( 'the modifications, made by applyModelMixin() to the models, persist and are present in the models after instantiation', function () { expect( models[0]._detectedPickyType ).toEqual( collection._pickyType ); expect( models[1]._detectedPickyType ).toEqual( collection._pickyType ); expect( models[2]._detectedPickyType ).toEqual( collection._pickyType ); } ); } ); } ); describe( 'existing models are passed in, with the Select.Me mixin already applied', function () { var collection; beforeEach( function () { f.creationMethod = "new"; f.modelDataSets = _.map( f.attributeSets, function ( attributeSet ) { return new SelectMeModel( attributeSet ); } ); spyOn( Backbone.Select.Me.custom, "applyModelMixin" ).and.callThrough(); collection = f.createPopulatedCollection( f.modelDataSets ); } ); afterEach( function () { collection.close(); } ); it( 'During instantiation, applyModelMixin() is not called', function () { expect( Backbone.Select.Me.custom.applyModelMixin ).not.toHaveBeenCalled(); } ); } ); } ); } ); } );