UNPKG

backbone.select

Version:

Selecting Backbone models; handling selections in Backbone collections.

985 lines (771 loc) 98.3 kB
describe( "Models shared between multiple collections: adding and removing models with set()", function () { var Model, SingleSelectCollection, MultiSelectCollection; beforeAll( function () { limitJasmineRecursiveScreenOutput(); } ); afterAll( function () { restoreJasmineRecursiveScreenOutput(); } ); beforeEach( function () { Model = Backbone.Model.extend( { initialize: function () { Backbone.Select.Me.applyTo( this ); } } ); SingleSelectCollection = Backbone.Collection.extend( { initialize: function ( models ) { Backbone.Select.One.applyTo( this, models ); } } ); MultiSelectCollection = Backbone.Collection.extend( { initialize: function ( models ) { Backbone.Select.Many.applyTo( this, models ); } } ); } ); describe( "when a selected model is added with set()", function () { var model1, model2, model3, model4, model5, singleCollectionA, singleCollectionB, multiCollectionA; beforeEach( function () { model1 = new Model(); model2 = new Model(); model3 = new Model(); model4 = new Model(); model5 = new Model(); singleCollectionA = new SingleSelectCollection( [model1, model2] ); multiCollectionA = new MultiSelectCollection( [model2, model3, model4] ); model1.select(); model3.select(); model4.select(); model5.select(); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( model4, "trigger" ).and.callThrough(); spyOn( model5, "trigger" ).and.callThrough(); spyOn( singleCollectionA, "trigger" ).and.callThrough(); spyOn( multiCollectionA, "trigger" ).and.callThrough(); } ); afterEach( function () { singleCollectionA.close(); if ( singleCollectionB ) singleCollectionB.close(); multiCollectionA.close(); } ); it( "should be selected in a single-select collection it is added to", function () { singleCollectionA.set( model5 ); expect( singleCollectionA.selected ).toBe( model5 ); } ); it( "should deselect a previously selected model as it is removed, provided that the model is not shared by another collection (set() in single-select collection)", function () { singleCollectionA.set( model5 ); expect( model1.selected ).toBe( false ); } ); it( "should leave a previously selected model selected as it is removed, provided that the model is shared by another collection (set() in single-select collection)", function () { model2.select(); singleCollectionA.set( model5 ); expect( model2.selected ).toBe( true ); } ); it( "should be added to the selected models in a multi-select collection it is added to", function () { multiCollectionA.set( model5 ); expect( multiCollectionA.selected[model5.cid] ).not.toBeUndefined(); } ); it( "should deselect any previously selected models as they are removed, provided that the models are not shared by another collection (set() in multi-select collection)", function () { multiCollectionA.set( model5 ); expect( model3.selected ).toBe( false ); expect( model4.selected ).toBe( false ); } ); it( "should leave any previously selected models selected as they are removed, provided that the models are shared by another collection (set() in multi-select collection)", function () { model2.select(); multiCollectionA.set( model5 ); expect( model2.selected ).toBe( true ); } ); it( "should remain selected itself", function () { singleCollectionA.set( model5 ); expect( model5.selected ).toBe( true ); } ); it( 'should not trigger a selected event on the model when added to a single-select collection', function () { singleCollectionA.set( model5 ); expect( model5.trigger ).not.toHaveBeenCalledWithInitial( "selected" ); } ); it( 'should not trigger a reselected event on the model when added to a single-select collection', function () { // The reselect event implies some sort of active 'select' action. Simply // registering the model status in the new collection does not belong into // that category. It does not reflect the action of a user reaffirming a // selection. singleCollectionA.set( model5 ); expect( model5.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'should not trigger a selected event on the model when added to a multi-select collection', function () { multiCollectionA.set( model5 ); expect( model5.trigger ).not.toHaveBeenCalledWithInitial( "selected" ); } ); it( 'should not trigger a reselected event on the model when added to a multi-select collection', function () { // See the comment above for the rationale. multiCollectionA.set( model5 ); expect( model5.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'should trigger a deselected event on a previously selected model as it is removed, provided that the model is not shared by another collection (set() in single-select collection)', function () { singleCollectionA.set( model5 ); expect( model1.trigger ).toHaveBeenCalledWithInitial( "deselected", model1 ); } ); it( 'should not trigger a deselected event on a previously selected model as it is removed, provided that the model is shared by another collection (set() in single-select collection)', function () { model2.select(); model2.trigger.calls.reset(); singleCollectionA.set( model5 ); expect( model2.trigger ).not.toHaveBeenCalled_ignoringEvents( "remove" ); } ); it( 'should trigger a deselected event on any previously selected models as they are removed, provided that the models are not shared by another collection (set() in multi-select collection)', function () { multiCollectionA.set( model5 ); expect( model3.trigger ).toHaveBeenCalledWithInitial( "deselected", model3 ); expect( model4.trigger ).toHaveBeenCalledWithInitial( "deselected", model4 ); } ); it( 'should not trigger a deselected event on any previously selected models as they are removed, provided that the models are shared by another collection (set() in multi-select collection)', function () { model2.select(); model2.trigger.calls.reset(); multiCollectionA.set( model5 ); expect( model2.trigger ).not.toHaveBeenCalled_ignoringEvents( "remove" ); } ); it( 'should trigger a select:one event when added to a single-select collection', function () { singleCollectionA.set( model5 ); expect( singleCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:one", model5, singleCollectionA ); } ); it( 'should trigger a deselect:one event when a previously selected model is removed from a single-select collection, provided that the removed model is not shared by another collection', function () { // The removed model becomes deselected, then. singleCollectionA.set( model5 ); expect( singleCollectionA.trigger ).toHaveBeenCalledWithInitial( "deselect:one", model1, singleCollectionA ); } ); it( 'should trigger a deselect:one event when a previously selected model is removed from a single-select collection, even if the removed model is shared by another collection', function () { // The removed model remains selected then, but still should trigger a deselect:one event on the collection // it is removed from. model2.select(); singleCollectionA.trigger.calls.reset(); singleCollectionA.set( model5 ); expect( singleCollectionA.trigger ).toHaveBeenCalledWithInitial( "deselect:one", model2, singleCollectionA ); } ); it( 'should not trigger a reselect:one event when added to a single-select collection', function () { singleCollectionA.set( model5 ); expect( singleCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); it( 'should trigger a select:all event when added to a multi-select collection', function () { // Note that only the added model is contained in the diff object, not the removed ones which are deselected. // Each removed model fires its own `remove` event, along with a select:all/some/none event of its own. // Likewise, each added model gets its own `add` event, followed by an update of the selection. multiCollectionA.set( model5 ); expect( multiCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [model5], deselected: [] }, multiCollectionA ); } ); it( 'should trigger a select:some or select:all event as previous selected models are removed one by one, in a multi-select collection', function () { multiCollectionA.set( model5 ); expect( multiCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [], deselected: [model3] }, multiCollectionA ); expect( multiCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:none", { selected: [], deselected: [model4] }, multiCollectionA ); } ); it( 'should not trigger a reselect:any event when added to a multi-select collection', function () { multiCollectionA.set( model5 ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:any" ); } ); it( 'should not trigger a select:all, select:some or select:none event in another multi-select collection holding the model', function () { model5 = new Model(); singleCollectionB = new SingleSelectCollection( [model5] ); model5.select(); singleCollectionB.set( model4 ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:none" ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:some" ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:all" ); } ); it( 'should not trigger a reselect:one event on another single-select collection holding the model', function () { singleCollectionB = new SingleSelectCollection(); singleCollectionB.set( model1 ); expect( singleCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); it( 'should not trigger a reselect:any event on another multi-select collection holding the model', function () { singleCollectionA.set( model2 ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:any" ); } ); } ); describe( "when a selected model is added with set(), with options.silent enabled", function () { var model1, model2, model3, model4, model5, singleCollectionA, singleCollectionB, multiCollectionA; beforeEach( function () { model1 = new Model(); model2 = new Model(); model3 = new Model(); model4 = new Model(); model5 = new Model(); singleCollectionA = new SingleSelectCollection( [model1, model2] ); multiCollectionA = new MultiSelectCollection( [model2, model3, model4] ); model1.select(); model3.select(); model4.select(); model5.select(); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( model4, "trigger" ).and.callThrough(); spyOn( model5, "trigger" ).and.callThrough(); spyOn( singleCollectionA, "trigger" ).and.callThrough(); spyOn( multiCollectionA, "trigger" ).and.callThrough(); } ); afterEach( function () { singleCollectionA.close(); if ( singleCollectionB ) singleCollectionB.close(); multiCollectionA.close(); } ); it( "should be selected in a single-select collection it is added to", function () { singleCollectionA.set( model5, { silent: true } ); expect( singleCollectionA.selected ).toBe( model5 ); } ); it( "should deselect a previously selected model as it is removed, provided that the model is not shared by another collection (set() in single-select collection)", function () { singleCollectionA.set( model5, { silent: true } ); expect( model1.selected ).toBe( false ); } ); it( "should leave a previously selected model selected as it is removed, provided that the model is shared by another collection (set() in single-select collection)", function () { model2.select(); singleCollectionA.set( model5, { silent: true } ); expect( model2.selected ).toBe( true ); } ); it( "should be added to the selected models in a multi-select collection it is added to", function () { multiCollectionA.set( model5, { silent: true } ); expect( multiCollectionA.selected[model5.cid] ).not.toBeUndefined(); } ); it( "should deselect any previously selected models as they are removed, provided that the models are not shared by another collection (set() in multi-select collection)", function () { multiCollectionA.set( model5, { silent: true } ); expect( model3.selected ).toBe( false ); expect( model4.selected ).toBe( false ); } ); it( "should leave any previously selected models selected as they are removed, provided that the models are shared by another collection (set() in multi-select collection)", function () { model2.select(); multiCollectionA.set( model5, { silent: true } ); expect( model2.selected ).toBe( true ); } ); it( "should remain selected itself", function () { singleCollectionA.set( model5, { silent: true } ); expect( model5.selected ).toBe( true ); } ); it( 'should not trigger an event on the model when added to a single-select collection', function () { singleCollectionA.set( model5, { silent: true } ); expect( model5.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on the model when added to a multi-select collection', function () { multiCollectionA.set( model5, { silent: true } ); expect( model5.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a deselected event on a previously selected model as it is removed, even if the model is not shared by another collection (set() in single-select collection)', function () { // ... so the model actually becomes deselected (would stay selected if it were part of another collection). singleCollectionA.set( model5, { silent: true } ); expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a deselected event on any previously selected models as they are removed, even if the models are not shared by another collection (set() in multi-select collection)', function () { // See preceding test. multiCollectionA.set( model5, { silent: true } ); expect( model3.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); expect( model4.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a collection event when added to a single-select collection', function () { // We ignore the "@bbs:remove:silent" and "@bbs:add:silent" events here, which are always fired during an // otherwise silent removal and addition. singleCollectionA.set( model5, { silent: true } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "@bbs:remove:silent", "@bbs:add:silent" ); } ); it( 'should not trigger a collection event when added to a multi-select collection', function () { // We ignore the "@bbs:remove:silent" and "@bbs:add:silent" events here, which are always fired during an // otherwise silent removal and addition. multiCollectionA.set( model5, { silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "@bbs:remove:silent", "@bbs:add:silent" ); } ); it( 'should not trigger a collection event in another multi-select collection holding the model', function () { model5 = new Model(); singleCollectionB = new SingleSelectCollection( [model5] ); model5.select(); singleCollectionB.set( model4, { silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a collection event on another single-select collection holding the model', function () { singleCollectionB = new SingleSelectCollection(); singleCollectionB.set( model1, { silent: true } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); } ); describe( "when a selected model is added with set(), with options.remove set to false", function () { var model1, model2, model3, model4, model5, singleCollectionA, singleCollectionB, multiCollectionA; beforeEach( function () { model1 = new Model(); model2 = new Model(); model3 = new Model(); model4 = new Model(); singleCollectionA = new SingleSelectCollection( [model1] ); multiCollectionA = new MultiSelectCollection( [model1, model2, model3] ); model1.select(); model2.select(); model3.select(); model4.select(); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( model4, "trigger" ).and.callThrough(); spyOn( singleCollectionA, "trigger" ).and.callThrough(); spyOn( multiCollectionA, "trigger" ).and.callThrough(); } ); afterEach( function () { singleCollectionA.close(); if ( singleCollectionB ) singleCollectionB.close(); multiCollectionA.close(); } ); it( "should be selected in a single-select collection it is added to", function () { singleCollectionA.set( model2, { remove: false } ); expect( singleCollectionA.selected ).toBe( model2 ); } ); it( "should deselect any previously selected model in a single-select collection", function () { singleCollectionA.set( model2, { remove: false } ); expect( model1.selected ).toBe( false ); } ); it( "should be added to the selected models in a multi-select collection", function () { multiCollectionA.set( model4, { remove: false } ); expect( multiCollectionA.selected[model4.cid] ).not.toBeUndefined(); } ); it( "should deselect other selected models in a multi-select collection if they are shared with the single-select collection the new model is added to", function () { singleCollectionA.set( model2, { remove: false } ); expect( multiCollectionA.selected[model1.cid] ).toBeUndefined(); } ); it( "should leave other selected models untouched in a multi-select collection, provided they are not shared with the single-select collection the new model is added to", function () { singleCollectionA.set( model2, { remove: false } ); expect( multiCollectionA.selected[model3.cid] ).not.toBeUndefined(); } ); it( "should remain selected itself", function () { singleCollectionA.set( model2, { remove: false } ); expect( model2.selected ).toBe( true ); } ); it( 'should not trigger a selected event on the model when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( model2.trigger ).not.toHaveBeenCalledWithInitial( "selected" ); } ); it( 'should not trigger a reselected event on the model when added to a single-select collection', function () { // The reselect event implies some sort of active 'select' action. Simply // registering the model status in the new collection does not belong into // that category. It does not reflect the action of a user reaffirming a // selection. singleCollectionA.set( model2, { remove: false } ); expect( model2.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'should not trigger a selected event on the model when added to a multi-select collection', function () { multiCollectionA.set( model4, { remove: false } ); expect( model4.trigger ).not.toHaveBeenCalledWithInitial( "selected" ); } ); it( 'should not trigger a reselected event on the model when added to a multi-select collection', function () { // See the comment above for the rationale. multiCollectionA.set( model4, { remove: false } ); expect( model4.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'should trigger a deselected event on the previously selected model when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( model1.trigger ).toHaveBeenCalledWith( "deselected", model1, { label: "selected" } ); } ); it( 'should not trigger a deselected event on a previously selected model when added to a multi-select collection, as long as old and new models are not shared with a single-select collection', function () { // ... in which case only one of them can remain selected, of course. See test below. multiCollectionA.set( model4, { remove: false } ); expect( model1.trigger ).not.toHaveBeenCalledWithInitial( "deselected" ); } ); it( 'should trigger a select:one event when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( singleCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:one", model2, singleCollectionA ); // or (full signature) // expect( singleCollectionA.trigger ).toHaveBeenCalledWith( "select:one", model2, singleCollectionA, { label: "selected", _externalEvent: "add" } ); } ); it( 'should trigger a deselect:one event when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( singleCollectionA.trigger ).toHaveBeenCalledWithInitial( "deselect:one", model1, singleCollectionA ); // or (full signature) // expect( singleCollectionA.trigger ).toHaveBeenCalledWith( "deselect:one", model1, singleCollectionA, { label: "selected", _externalEvent: "add" } ); } ); it( 'should not trigger a reselect:one event when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); it( 'should trigger a select:some or select:all event when added to a multi-select collection', function () { multiCollectionA.set( model4, { remove: false } ); expect( multiCollectionA.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [model4], deselected: [] }, multiCollectionA ); // or (full signature) // expect( multiCollectionA.trigger ).toHaveBeenCalledWith( "select:all", { selected: [model4], deselected: [] }, multiCollectionA, { label: "selected", _externalEvent: "add" } ); } ); it( 'should not trigger a reselect:any event when added to a multi-select collection', function () { multiCollectionA.set( model4, { remove: false } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:any" ); } ); it( 'should trigger a select:some or select:none event when the addition is inducing a deselection in another multi-select collection', function () { singleCollectionA.set( model2, { remove: false } ); expect( multiCollectionA.trigger ).toHaveBeenCalledWith( "select:some", { selected: [], deselected: [model1] }, multiCollectionA, { label: "selected" } ); } ); it( 'should not trigger a select:all, select:some or select:none event when the addition does not deselect a model in another multi-select collection', function () { model5 = new Model(); singleCollectionB = new SingleSelectCollection( [model5] ); model5.select(); singleCollectionB.set( model2, { remove: false } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:none" ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:some" ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "select:all" ); } ); it( 'should not trigger a reselect:one event on another single-select collection holding the model', function () { singleCollectionB = new SingleSelectCollection(); singleCollectionB.set( model1, { remove: false } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); it( 'should not trigger a reselect:any event on another multi-select collection holding the model', function () { singleCollectionA.set( model2, { remove: false } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledWithInitial( "reselect:any" ); } ); } ); describe( "when a selected model is added with set(), with options.remove set to false, and with options.silent enabled", function () { var model1, model2, model3, model4, model5, singleCollectionA, singleCollectionB, multiCollectionA; beforeEach( function () { model1 = new Model(); model2 = new Model(); model3 = new Model(); model4 = new Model(); singleCollectionA = new SingleSelectCollection( [model1] ); multiCollectionA = new MultiSelectCollection( [model1, model2, model3] ); model1.select(); model2.select(); model3.select(); model4.select(); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( model4, "trigger" ).and.callThrough(); spyOn( singleCollectionA, "trigger" ).and.callThrough(); spyOn( multiCollectionA, "trigger" ).and.callThrough(); } ); afterEach( function () { singleCollectionA.close(); if ( singleCollectionB ) singleCollectionB.close(); multiCollectionA.close(); } ); it( "should be selected in a single-select collection it is added to", function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( singleCollectionA.selected ).toBe( model2 ); } ); it( "should deselect any previously selected model in a single-select collection", function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( model1.selected ).toBe( false ); } ); it( "should be added to the selected models in a multi-select collection", function () { multiCollectionA.set( model4, { remove: false, silent: true } ); expect( multiCollectionA.selected[model4.cid] ).not.toBeUndefined(); } ); it( "should deselect other selected models in a multi-select collection if they are shared with the single-select collection the new model is added to", function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( multiCollectionA.selected[model1.cid] ).toBeUndefined(); } ); it( "should leave other selected models untouched in a multi-select collection, provided they are not shared with the single-select collection the new model is added to", function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( multiCollectionA.selected[model3.cid] ).not.toBeUndefined(); } ); it( "should remain selected itself", function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( model2.selected ).toBe( true ); } ); it( 'should not trigger an event on the model when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( model2.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on the model when added to a multi-select collection', function () { multiCollectionA.set( model4, { remove: false, silent: true } ); expect( model4.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on the previously selected model when added to a single-select collection', function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on a previously selected model when added to a multi-select collection, as long as old and new models are not shared with a single-select collection', function () { // ... in which case only one of them can remain selected, of course. See test below for that. multiCollectionA.set( model4, { remove: false, silent: true } ); expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a single-select collection event when added to a single-select collection', function () { // We ignore the "@bbs:add:silent" event here, which is always fired during an otherwise silent addition. singleCollectionA.set( model2, { remove: false, silent: true } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalledOnce_ignoringInternalEventsAnd( "@bbs:add:silent" ); } ); it( 'should not trigger a multi-select collection event when added to a multi-select collection', function () { // We ignore the "@bbs:add:silent" event here, which is always fired during an otherwise silent addition. multiCollectionA.set( model4, { remove: false, silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalledOnce_ignoringInternalEventsAnd( "@bbs:add:silent" ); } ); it( 'should not trigger a multi-select collection event when the addition is inducing a deselection in another multi-select collection', function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger a multi-select collection event when the addition does not deselect a model in another multi-select collection', function () { model5 = new Model(); singleCollectionB = new SingleSelectCollection( [model5] ); model5.select(); singleCollectionB.set( model2, { remove: false, silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on another single-select collection holding the model', function () { singleCollectionB = new SingleSelectCollection(); singleCollectionB.set( model1, { remove: false, silent: true } ); expect( singleCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'should not trigger an event on another multi-select collection holding the model', function () { singleCollectionA.set( model2, { remove: false, silent: true } ); expect( multiCollectionA.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); } ); describe( 'when a selected model is added with set(), together with a selected model already in the collection', function () { var model1, model2, collection; beforeEach( function () { model1 = new Model(); model2 = new Model(); model1.select(); model2.select(); } ); describe( 'in a Select.One collection', function () { beforeEach( function () { collection = new SingleSelectCollection( [model2] ); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( collection, "trigger" ).and.callThrough(); } ); afterEach( function () { collection.close(); } ); describe( 'with the existing model passed in first', function () { beforeEach( function () { collection.set( [model2, model1] ); } ); it( 'the new model remains selected', function () { expect( model1.selected ).toBe( true ); } ); it( 'the existing model is deselected', function () { expect( model2.selected ).toBe( false ); } ); it( 'the new model is selected in the collection', function () { expect( collection.selected ).toBe( model1 ); } ); it( 'no event is triggered on the new model', function () { expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "add" ); } ); it( 'a deselected event is triggered on the existing model', function () { expect( model2.trigger ).toHaveBeenCalledWithInitial( "deselected", model2 ); } ); it( 'no reselected event is triggered on the existing model', function () { expect( model2.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'a select:one collection event is triggered for the new model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "select:one", model1, collection ); } ); it( 'a deselect:one collection event is triggered for the existing model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "deselect:one", model2, collection ); } ); it( 'no reselect:one event is triggered', function () { expect( collection.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); } ); describe( 'with the existing model passed in second', function () { // NB This may be surprising, but the order does NOT matter. The result is exactly the same as in the // previous test, where the existing model was passed in first. // // One might expect that the order would matter: that the first model gets deselected by the second - // the last one wins. But that doesn't happen because set() just acts as a wrapper around three separate // operations, executed in order: // // - remove() models which are not in the list // - update models which already exist in the collection // - add() the new models // // That sequence of operation takes precedence over the order of models in the array. Additions are // last, so a selected model which is added "wins" and deselects an existing selected model. beforeEach( function () { collection.set( [model1, model2] ); } ); it( 'the new model remains selected', function () { expect( model1.selected ).toBe( true ); } ); it( 'the existing model is deselected', function () { expect( model2.selected ).toBe( false ); } ); it( 'the new model is selected in the collection', function () { expect( collection.selected ).toBe( model1 ); } ); it( 'no event is triggered on the new model', function () { expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "add" ); } ); it( 'a deselected event is triggered on the existing model', function () { expect( model2.trigger ).toHaveBeenCalledWithInitial( "deselected", model2 ); } ); it( 'no reselected event is triggered on the existing model', function () { expect( model2.trigger ).not.toHaveBeenCalledWithInitial( "reselected" ); } ); it( 'a select:one collection event is triggered for the new model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "select:one", model1, collection ); } ); it( 'a deselect:one collection event is triggered for the existing model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "deselect:one", model2, collection ); } ); it( 'no reselect:one event is triggered', function () { expect( collection.trigger ).not.toHaveBeenCalledWithInitial( "reselect:one" ); } ); } ); } ); describe( 'in a Select.Many collection', function () { var model3; beforeEach( function () { model3 = new Model(); } ); afterEach( function () { collection.close(); } ); describe( 'which also contains a third model, not selected, to be removed on set()', function () { beforeEach( function () { collection = new MultiSelectCollection( [model2, model3] ); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( collection, "trigger" ).and.callThrough(); collection.set( [model1, model2] ); } ); it( 'the new model remains selected', function () { expect( model1.selected ).toBe( true ); } ); it( 'the existing model remains selected', function () { expect( model2.selected ).toBe( true ); } ); it( 'the existing and new model are selected in the collection', function () { var expected = {}; expected[model1.cid] = model1; expected[model2.cid] = model2; expect( collection.selected ).toEqual( expected ); } ); it( 'no event is triggered on the new model', function () { expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "add" ); } ); it( 'no event is triggered on the existing model', function () { expect( model2.trigger ).not.toHaveBeenCalled(); } ); it( 'a select:all collection event is triggered for the new model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [model1], deselected: [] }, collection ); } ); it( 'no collection event is triggered upon removal of the third model', function () { // The collection _status_ changes from some models being selected to all models being selected, but // no selection/deselection is involved. We don't have selection events when the status changes, we // have them when a selection changes. Put differently, we never have events when both diff.selected // and diff.deselected would be empty. // // So the select:all event is only called once, later on, when the new model is added to the // collection. expect( collection.trigger ).toHaveBeenCalledOnceForEvents( "select:all" ); } ); it( 'no other selection-related events are triggered in the collection', function () { // We are ignoring namespaced sub events for the labels here (select:all:selected). // // The one select:all event is triggered for the addition of the new model (model1), and has already // been covered in the preceding test. expect( collection.trigger ).not.toHaveBeenCalledForEvents( "select:none", "select:some", "reselect:any" ); } ); } ); describe( 'which also contains a third model, selected, to be removed on set()', function () { beforeEach( function () { collection = new MultiSelectCollection( [model2, model3] ); model3.select(); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( model3, "trigger" ).and.callThrough(); spyOn( collection, "trigger" ).and.callThrough(); collection.set( [model1, model2] ); } ); it( 'the new model remains selected', function () { expect( model1.selected ).toBe( true ); } ); it( 'the existing model remains selected', function () { expect( model2.selected ).toBe( true ); } ); it( 'the existing and new model are selected in the collection', function () { var expected = {}; expected[model1.cid] = model1; expected[model2.cid] = model2; expect( collection.selected ).toEqual( expected ); } ); it( 'no event is triggered on the new model', function () { expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEventsAnd( "add" ); } ); it( 'no event is triggered on the existing model', function () { expect( model2.trigger ).not.toHaveBeenCalled(); } ); it( 'a select:all collection event is triggered upon removal of the third model', function () { // It may be counter-intuitive to see a select:all event after a selected model is removed, but // after the removal of the model is complete, the selection has shrunk, and the one remaining model // (model2) is selected, so: selected:all. expect( collection.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [], deselected: [model3] }, collection ); } ); it( 'a select:all collection event is triggered for the new model', function () { expect( collection.trigger ).toHaveBeenCalledWithInitial( "select:all", { selected: [model1], deselected: [] }, collection ); } ); it( 'no other selection-related events are triggered in the collection', function () { // We are ignoring namespaced sub events for the labels here (select:all:selected). // // The two select:all events are triggered for the removal of the excess model (model3), and for the // addition of the new model (model1), and have already been covered in the preceding tests. expect( collection.trigger ).not.toHaveBeenCalledForEvents( "select:none", "select:some", "reselect:any" ); expect( collection.trigger ).toHaveBeenCalledTwiceForEvents( "select:all" ); } ); } ); } ); } ); describe( 'when a selected model is added with set(), together with a selected model already in the collection, with options.silent enabled', function () { var model1, model2, collection; beforeEach( function () { model1 = new Model(); model2 = new Model(); model1.select(); model2.select(); } ); describe( 'in a Select.One collection', function () { beforeEach( function () { collection = new SingleSelectCollection( [model2] ); spyOn( model1, "trigger" ).and.callThrough(); spyOn( model2, "trigger" ).and.callThrough(); spyOn( collection, "trigger" ).and.callThrough(); } ); afterEach( function () { collection.close(); } ); describe( 'with the existing model passed in first', function () { beforeEach( function () { collection.set( [model2, model1], { silent: true } ); } ); it( 'the new model remains selected', function () { expect( model1.selected ).toBe( true ); } ); it( 'the existing model is deselected', function () { expect( model2.selected ).toBe( false ); } ); it( 'the new model is selected in the collection', function () { expect( collection.selected ).toBe( model1 ); } ); it( 'no event is triggered on the new model', function () { expect( model1.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'no event is triggered on the existing model', function () { expect( model2.trigger ).not.toHaveBeenCalled_ignoringInternalEvents(); } ); it( 'no event is triggered in the collection', function () { // We ignore the "@bbs:add:silent" event here, which is always fired during an otherwise silent // addition. expect( collection.trigger ).not.toHaveBeenCalledOnce_ignoringInternalEventsAnd( "@bbs:add:silent" ); } ); } ); describe( 'with the existing model passed in second', function () { // NB This may be surprising, but the order does NOT matter. The result is exactly the same as in the // previous test, where the existing model was passed in first. // // One might expect that the order would matter: that the first model gets deselected by the second - // the last one wins. But that doesn't happen because set() just acts as a wrapper around three separate // operations, executed in order: // // - remove() models which are not in the list // - update models which already exist in the collection // - add() the new models // // That sequence of operation takes precedence over the order of models in the array. Additions are // last, so a selected model which is added "wins" and deselects an existing selected model. beforeEach( function () { collection.set( [model1, model2], { s