transactional
Version:
Reactive objects with transactional updates and automatic serialization
291 lines (224 loc) • 10.1 kB
JavaScript
var Nested = require( '../nestedtypes' ),
expect = require( 'chai' ).expect,
_ = require( 'underscore' ),
sinon = require( 'sinon' );
describe( 'Nested Models and Collections', function(){
function shouldFireChangeOnce( model, attr, todo ){
var change = sinon.spy(),
attrs = attr.split( ' ' );
model.on( 'change', change );
var changeAttrs = _.map( attrs, function( name ){
var changeName = sinon.spy();
model.on( 'change:' + name, changeName );
return changeName;
});
todo( model );
expect( change ).to.be.calledOnce;
model.off( change );
_.each( changeAttrs, function( spy ){
expect( spy ).to.be.calledOnce;
model.off( spy );
});
}
var A = Nested.Model.extend({
attributes : {
a : 1,
b : 2
}
});
var B = Nested.Model.extend({
attributes :{
first : A.has.parse( function( resp ){
return { a : resp.a + 1, b : resp.b + 1 };
}),
second : A.has.triggerWhenChanged( false ),
c : A.Collection
}
});
function createSpies(){
var collection = sinon.spy(),
model = sinon.spy();
A.Collection.prototype.parse = function( data ){
collection();
return data;
};
A.prototype.parse = function( data ){
model();
return data;
};
return { model : model, collection : collection };
}
describe( 'Nested parse method', function(){
describe( 'Nested model', function(){
it( 'set _owner property', function(){
var m = new B();
expect( m.first._owner ).to.eql( m );
expect( m.c._owner ).to.eql( m );
var a = new A();
var _first = m.first;
m.first = a;
expect( _first._owner ).to.not.ok;
expect( m.first._owner ).to.eql( m );
});
it( 'invoke "parse" on construction', function(){
var spies = createSpies();
var m = new B({
first : {id : 1, a : 2, b : 2}
}, { parse : true });
expect( spies.model ).to.be.calledOnce;
expect( m.first.a ).to.eql( 3 );
expect( m.first.b ).to.eql( 3 );
});
it( 'invoke "parse" on set', function(){
var spies = createSpies();
var m = new B();
m.set( 'first', {id : 1, a : 2, b : 2}, { parse : true });
expect( spies.model ).to.be.calledOnce;
});
it( 'invoke "parse" on set when value is nul' , function(){
var spies = createSpies();
var m = new B({ first : null });
m.set( 'first', {id : 1, a : 1, b : 2}, { parse : true } );
expect( spies.model ).to.be.calledOnce;
});
});
describe( 'Nested collection', function(){
it( 'invoke "parse" on construction', function(){
var spies = createSpies();
var m = new B({
c : [{id : 1, a : 2, b : 2}]
}, { parse : true });
expect( spies.collection ).to.be.calledOnce;
expect( spies.model ).to.be.calledOnce;
});
it( 'invoke "parse" on set', function(){
var spies = createSpies();
var m = new B();
m.set( 'c', [{id : 1, a : 1, b : 2}], { parse : true } );
expect( spies.collection ).to.be.calledOnce;
expect( spies.model ).to.be.calledOnce;
});
it( 'invoke "parse" on set when value is null', function(){
var spies = createSpies();
var m = new B({ c : null });
m.set( 'c', [{id : 1, a : 1, b : 2}], { parse : true } );
expect( spies.collection ).to.be.calledOnce;
expect( spies.model ).to.be.calledOnce;
});
})
});
describe( 'Nested.Model attribute type cast on assignment', function(){
describe( 'when current attribute value is not null (deep update)', function(){
it( 'delegate update to nested model\'s .set', function(){
var m = new B(),
id = m.first.cid;
m.first = {id : 1, a : 1, b : 2};
expect( id ).to.eql( m.first.cid );
expect( m.first.id ).to.eql( 1 );
});
it( 'triggers single "change" and single "change:attr" events if nested model is changed', function(){
var m = new B();
shouldFireChangeOnce( m, 'first', function(){
m.first = {id : 1, a : 1, b : 2};
});
} );
});
describe( 'when current attribute value is empty (null)', function(){
it( 'creates new model', function(){
var m = new B({ first : null });
m.first = { id : 1, a : 1, b : 2 };
expect( m.first.id ).to.eql( 1 );
});
it( 'triggers "change", "change:attr", and "replace:attr" events', function(){
var m = new B({ first : null } ),
replace = sinon.spy();
m.on( 'replace:first', replace );
shouldFireChangeOnce( m, 'first', function(){
m.first = {id : 1, a : 1, b : 2};
});
expect( replace ).to.be.calledOnce;
});
});
});
describe( 'Nested.Collection attribute type cast on assignment', function(){
describe( 'when current attribute value is not null (deep update)', function(){
it( 'delegate update to nested collection\'s .set', function(){
var m = new B(),
id = m.c._listenId;
m.c = [{id : 1, a : 1, b : 2}];
expect( id ).to.eql( m.c._listenId );
expect( m.c.first().id ).to.eql( 1 );
});
it( 'triggers single "change" and single "change:attr" events when nested collection is changed', function(){
var m = new B();
shouldFireChangeOnce( m, 'c', function(){
m.c = [{id : 1, a : 1, b : 2}];
});
});
});
describe( 'when current attribute value is empty (null)', function(){
it( 'creates new collection', function(){
var m = new B({ c : null });
m.c = [{id : 1, a : 1, b : 2}];
expect( m.c.first().id ).to.eql( 1 );
});
it( 'triggers "change", "change:attr", and "replace:attr" events', function(){
var m = new B({ c : null } ),
replace = sinon.spy();
m.on( 'replace:c', replace );
shouldFireChangeOnce( m, 'c', function(){
m.c = [{id : 1, a : 1, b : 2}];
});
expect( replace ).to.be.calledOnce;
});
});
});
describe( 'nested model and collection event bubbling', function(){
it( 'bubble "change" event from nested model', function(){
var m = new B();
shouldFireChangeOnce( m, 'first', function(){
m.first.a = 2;
});
});
it( 'send single "change" event in a transaction', function(){
var m = new B();
shouldFireChangeOnce( m, 'first', function(){
m.first.transaction( function(){
this.a = 7;
this.b = 7;
});
});
});
it( 'send single "change" event in a nested transaction', function(){
var m = new B();
shouldFireChangeOnce( m, 'first', function(){
m.transaction( function(){
m.first.a = 7;
m.first.b = 7;
});
});
});
it( 'generate "change" event on any nested collection modification', function(){
var m = new B();
shouldFireChangeOnce( m, 'c', function(){
m.c.add({ id: 1, a: 1, b : 2 });
});
shouldFireChangeOnce( m, 'c', function(){
m.c.first().a = 2;
});
shouldFireChangeOnce( m, 'c', function(){
m.c.remove( m.c.first() );
});
});
it( 'may be disabled for selected nested attributes', function(){
var m = new B(),
spyTop = sinon.spy(),
spyBottom = sinon.spy();
m.on( 'change', spyTop );
m.second.on( 'change', spyBottom );
m.second.a = 5;
expect( spyBottom ).to.be.calledOnce;
expect( spyBottom ).to.be.notCalled;
});
})
});