// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Apple Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals module ok equals same test MyApp */ // test core array-mapping methods for ManyArray with ManyAttribute var storeKeys, rec, rec2, rec3, rec4; var foo1, foo2, foo3, bar1, bar2, bar3; module("SC.ManyAttribute core methods", { setup: function() { SC.RunLoop.begin(); MyApp = SC.Object.create({ store: SC.Store.create() }); MyApp.Foo = SC.Record.extend({ // test simple reading of a pass-through prop firstName: SC.Record.attr(String), // test mapping to another internal key otherName: SC.Record.attr(String, { key: "firstName" }), // test mapping Date date: SC.Record.attr(Date), // used to test default value defaultValue: SC.Record.attr(String, { defaultValue: "default" }), // test toMany relationships fooMany: SC.Record.toMany('MyApp.Foo', { supportNewRecords: false }), // test toMany relationships with different key fooManyKeyed: SC.Record.toMany('MyApp.Foo', { key: 'fooIds' }), // test many-to-many relationships with inverse barToMany: SC.Record.toMany('MyApp.Bar', { inverse: 'fooToMany', isMaster: YES, orderBy: 'name' }), // test many-to-one relationships with inverse barToOne: SC.Record.toMany('MyApp.Bar', { inverse: 'fooToOne', isMaster: NO }) }); MyApp.Bar = SC.Record.extend({ // test many-to-many fooToMany: SC.Record.toMany('MyApp.Foo', { inverse: 'barToMany', isMaster: NO, supportNewRecords: false }), // test many-to-one fooToOne: SC.Record.toOne('MyApp.Foo', { inverse: 'barToOne', isMaster: YES }) }); storeKeys = MyApp.store.loadRecords(MyApp.Foo, [ { guid: 1, firstName: "John", lastName: "Doe", barToMany: ['bar1'], barToOne: ['bar1', 'bar2'] }, { guid: 2, firstName: "Jane", lastName: "Doe", barToMany: ['bar1', 'bar2'], barToOne: [] }, { guid: 3, firstName: "Emily", lastName: "Parker", fooMany: [1,2], barToMany: ['bar2'], barToOne: [] }, { guid: 4, firstName: "Johnny", lastName: "Cash", fooIds: [1,2] } ]); MyApp.store.loadRecords(MyApp.Bar, [ { guid: "bar1", name: "A", fooToMany: [1,2], fooToOne: 1 }, { guid: "bar2", name: "Z", fooToMany: [2,3], fooToOne: 1 }, { guid: "bar3", name: "C" } ]); foo1 = rec = MyApp.store.find(MyApp.Foo, 1); foo2 = rec2 = MyApp.store.find(MyApp.Foo, 2); foo3 = rec3 = MyApp.store.find(MyApp.Foo, 3); rec4 = MyApp.store.find(MyApp.Foo, 4); equals(rec.storeKey, storeKeys[0], 'should find record'); bar1 = MyApp.store.find(MyApp.Bar, "bar1"); bar2 = MyApp.store.find(MyApp.Bar, 'bar2'); bar3 = MyApp.store.find(MyApp.Bar, 'bar3'); SC.RunLoop.end(); }, teardown: function() { MyApp = rec = rec2 = rec3 = foo1 = foo2 = foo3 = bar1 = bar2 = null; } }); // .......................................................... // READING // test("pass-through should return builtin value" ,function() { equals(rec.get('firstName'), 'John', 'reading prop should get attr value'); }); test("getting toMany relationship should map guid to real records", function() { var rec3 = MyApp.store.find(MyApp.Foo, 3); equals(rec3.get('id'), 3, 'precond - should find record 3'); equals(rec3.get('fooMany').objectAt(0), rec, 'should get rec1 instance for rec3.fooMany'); equals(rec3.get('fooMany').objectAt(1), rec2, 'should get rec2 instance for rec3.fooMany'); }); test("getting toMany relationship should map guid to real records when using different key", function() { var rec4 = MyApp.store.find(MyApp.Foo, 4); equals(rec4.get('id'), 4, 'precond - should find record 4'); equals(rec4.get('fooManyKeyed').objectAt(0), rec, 'should get rec1 instance for rec4.fooManyKeyed'); equals(rec4.get('fooManyKeyed').objectAt(1), rec2, 'should get rec2 instance for rec4.fooManyKeyed'); }); test("getting toMany relation should not change record state", function() { equals(rec3.get('status'), SC.Record.READY_CLEAN, 'precond - status should be READY_CLEAN'); var recs = rec3.get('fooMany'); ok(recs, 'rec3.get(fooMany) should return records'); equals(rec3.get('status'), SC.Record.READY_CLEAN, 'getting toMany should not change state'); }); test("reading toMany in chained store", function() { var recs1, recs2, store, rec3a; recs1 = rec3.get('fooMany'); store = MyApp.store.chain(); rec3a = store.find(rec3); recs2 = rec3a.get('fooMany'); same(recs2.getEach('storeKey'), recs1.getEach('storeKey'), 'returns arrays from chained and parent should be same'); ok(recs2 !== recs1, 'returned arrays should not be same instance'); }); test("reading a null relation", function() { // note: rec1 hash has NO array equals(rec.readAttribute('fooMany'), null, 'rec1.fooMany attr should be null'); var ret = rec.get('fooMany'); equals(ret.get('length'), 0, 'rec1.get(fooMany).length should be 0'); same(ret.getEach('storeKey'), [], 'rec1.get(fooMany) should return empty array'); }); // .......................................................... // WRITING // test("writing to a to-many relationship should update set guids", function() { var rec3 = MyApp.store.find(MyApp.Foo, 3); equals(rec3.get('id'), 3, 'precond - should find record 3'); equals(rec3.get('fooMany').objectAt(0), rec, 'should get rec1 instance for rec3.fooMany'); SC.RunLoop.begin(); rec3.set('fooMany', [rec2, rec4]); SC.RunLoop.end(); equals(rec3.get('fooMany').objectAt(0), rec2, 'should get rec2 instance for rec3.fooMany'); equals(rec3.get('fooMany').objectAt(1), rec4, 'should get rec4 instance for rec3.fooMany'); }); test("writing to a to-many relationship should update set guids when using a different key", function() { var rec4 = MyApp.store.find(MyApp.Foo, 4); equals(rec4.get('id'), 4, 'precond - should find record 4'); equals(rec4.get('fooManyKeyed').objectAt(0), rec, 'should get rec1 instance for rec4.fooManyKeyed'); SC.RunLoop.begin(); rec4.set('fooManyKeyed', [rec2, rec3]); SC.RunLoop.end(); ok(rec4.get('fooIds').isEqual([2,3]), 'should get array of guids (2, 3) for rec4.fooIds'); }); test("pushing an object to a to-many relationship attribute should update set guids", function() { var rec3 = MyApp.store.find(MyApp.Foo, 3); equals(rec3.get('id'), 3, 'precond - should find record 3'); equals(rec3.get('fooMany').length(), 2, 'should be 2 foo instances related'); SC.run(function () { rec3.get('fooMany').pushObject(rec4); }); equals(rec3.get('fooMany').length(), 3, 'should be 3 foo instances related'); equals(rec3.get('fooMany').objectAt(0), rec, 'should get rec instance for rec3.fooMany'); equals(rec3.get('fooMany').objectAt(1), rec2, 'should get rec2 instance for rec3.fooMany'); equals(rec3.get('fooMany').objectAt(2), rec4, 'should get rec4 instance for rec3.fooMany'); }); test("modifying a toMany array should mark the record as changed", function() { var recs = rec3.get('fooMany'); equals(rec3.get('status'), SC.Record.READY_CLEAN, 'precond - rec3.status should be READY_CLEAN'); ok(!!rec4, 'precond - rec4 should be defined'); SC.RunLoop.begin(); recs.pushObject(rec4); SC.RunLoop.end(); equals(rec3.get('status'), SC.Record.READY_DIRTY, 'record status should have changed to dirty'); }); test("Modifying a toMany array using replace", function() { var recs = rec.get('barToOne'), objectForRemoval = recs.objectAt(1); SC.run(function () { recs.replace(1, 1, null); // the object should be removed }); ok(objectForRemoval !== recs.objectAt(1), "record should not be present after a replace"); equals(bar2.get('fooToOne'), null, "record should have notified attribute of change"); }); test("modifying a toMany array within a nested store", function() { var child = MyApp.store.chain() ; // get a chained store var parentFooMany = rec3.get('fooMany'); // base foo many var childRec3 = child.find(rec3); var childFooMany = childRec3.get('fooMany'); // get the nested fooMany // save store keys before modifying for easy testing var expected = parentFooMany.getEach('storeKey'); // now trying modifying... var childRec4 = child.find(rec4); equals(childFooMany.get('length'), 2, 'precond - childFooMany should be like parent'); SC.run(function () { childFooMany.pushObject(childRec4); }); equals(childFooMany.get('length'), 3, 'childFooMany should have 1 more item'); SC.RunLoop.end(); // allow notifications to process, if there were any... same(parentFooMany.getEach('storeKey'), expected, 'parent.fooMany should not have changed yet'); equals(rec3.get('status'), SC.Record.READY_CLEAN, 'parent rec3 should still be READY_CLEAN'); expected = childFooMany.getEach('storeKey'); // update for after commit SC.RunLoop.begin(); child.commitChanges(); SC.RunLoop.end(); // NOTE: not getting fooMany from parent again also tests changing an array // underneath. Does it clear caches, etc? equals(parentFooMany.get('length'), 3, 'parent.fooMany length should have changed'); same(parentFooMany.getEach('storeKey'), expected, 'parent.fooMany should now have changed form child store'); equals(rec3.get('status'), SC.Record.READY_DIRTY, 'parent rec3 should now be READY_DIRTY'); }); test("should be able to modify an initially empty record", function() { same(rec.get('fooMany').getEach('storeKey'), [], 'precond - fooMany should be empty'); SC.run(function () { rec.get('fooMany').pushObject(rec4); }); same(rec.get('fooMany').getEach('storeKey'), [rec4.get('storeKey')], 'after edit should have new array'); }); test("Adding an unsaved record should throw an Error is supportNewRecords is false", function() { var foo; SC.run(function () { foo = MyApp.store.createRecord(MyApp.Foo, { firstName: "John", lastName: "Doe", barToMany: ['bar1'] }); }); try { SC.run(function () { bar1.get('fooToMany').pushObject(foo); }); ok(false, "Attempting to assign an unsaved record resulted in an error."); } catch (x) { ok(true, "Attempting to assign an unsaved record resulted in an error."); } }); // .......................................................... // MANY-TO-MANY RELATIONSHIPS // function checkAllClean() { SC.A(arguments).forEach(function(r) { equals(r.get('status'), SC.Record.READY_CLEAN, 'PRECOND - %@.status should be READY_CLEAN'.fmt(r.get('id'))); }, this); } test("removing a record from a many-to-many", function() { ok(foo1.get('barToMany').indexOf(bar1) >= 0, 'PRECOND - foo1.barToMany should contain bar1'); ok(bar1.get('fooToMany').indexOf(foo1) >= 0, 'PRECOND - bar1.fooToMany should contain foo1'); checkAllClean(foo1, bar1); SC.run(function () { foo1.get('barToMany').removeObject(bar1); }); ok(foo1.get('barToMany').indexOf(bar1) < 0, 'foo1.barToMany should NOT contain bar1'); ok(bar1.get('fooToMany').indexOf(foo1) < 0, 'bar1.fooToMany should NOT contain foo1'); equals(foo1.get('status'), SC.Record.READY_DIRTY, 'foo1.status should be READY_DIRTY'); equals(bar1.get('status'), SC.Record.READY_CLEAN, 'bar1.status should be READY_CLEAN'); }); test("removing a record from a many-to-many; other side", function() { ok(foo1.get('barToMany').indexOf(bar1) >= 0, 'PRECOND - foo1.barToMany should contain bar1'); ok(bar1.get('fooToMany').indexOf(foo1) >= 0, 'PRECOND - bar1.fooToMany should contain foo1'); checkAllClean(foo1, bar1); SC.run(function () { bar1.get('fooToMany').removeObject(foo1); }); ok(foo1.get('barToMany').indexOf(bar1) < 0, 'foo1.barToMany should NOT contain bar1'); ok(bar1.get('fooToMany').indexOf(foo1) < 0, 'bar1.fooToMany should NOT contain foo1'); equals(foo1.get('status'), SC.Record.READY_DIRTY, 'foo1.status should be READY_DIRTY'); equals(bar1.get('status'), SC.Record.READY_CLEAN, 'bar1.status should be READY_CLEAN'); }); test("adding a record to a many-to-many; bar side", function() { ok(foo2.get('barToMany').indexOf(bar3) < 0, 'PRECOND - foo1.barToMany should NOT contain bar1'); ok(bar3.get('fooToMany').indexOf(foo2) < 0, 'PRECOND - bar3.fooToMany should NOT contain foo1'); checkAllClean(foo2, bar3); SC.run(function () { bar3.get('fooToMany').pushObject(foo2); }); // v-- since bar3 is added through inverse, it should follow orderBy equals(foo2.get('barToMany').indexOf(bar3), 1, 'foo1.barToMany should contain bar1'); ok(bar3.get('fooToMany').indexOf(foo2) >= 0, 'bar1.fooToMany should contain foo1'); equals(foo2.get('status'), SC.Record.READY_DIRTY, 'foo1.status should be READY_DIRTY'); equals(bar1.get('status'), SC.Record.READY_CLEAN, 'bar1.status should be READY_CLEAN'); }); test("adding a record to a many-to-many; foo side", function() { ok(foo2.get('barToMany').indexOf(bar3) < 0, 'PRECOND - foo1.barToMany should NOT contain bar3'); ok(bar3.get('fooToMany').indexOf(foo2) < 0, 'PRECOND - bar3.fooToMany should NOT contain foo1'); checkAllClean(foo2, bar3); SC.run(function () { foo2.get('barToMany').pushObject(bar3); }); ok(foo2.get('barToMany').indexOf(bar3) >= 0, 'foo1.barToMany should contain bar3'); ok(bar3.get('fooToMany').indexOf(foo2) >= 0, 'bar1.fooToMany should contain foo3'); equals(foo2.get('status'), SC.Record.READY_DIRTY, 'foo1.status should be READY_DIRTY'); equals(bar1.get('status'), SC.Record.READY_CLEAN, 'bar3.status should be READY_CLEAN'); }); // .......................................................... // ONE-TO-MANY RELATIONSHIPS // test("removing a record from a one-to-many", function() { ok(foo1.get('barToOne').indexOf(bar1) >= 0, 'PRECOND - foo1.barToOne should contain bar1'); equals(bar1.get('fooToOne'), foo1, 'PRECOND - bar1.fooToOne should eq foo1'); checkAllClean(foo1, bar1); SC.run(function () { foo1.get('barToOne').removeObject(bar1); }); ok(foo1.get('barToOne').indexOf(bar1) < 0, 'foo1.barToOne should NOT contain bar1'); equals(bar1.get('fooToOne'), null, 'bar1.fooToOne should eq null'); equals(foo1.get('status'), SC.Record.READY_CLEAN, 'foo1.status should be READY_CLEAN'); equals(bar1.get('status'), SC.Record.READY_DIRTY, 'bar1.status should be READY_DIRTY'); }); test("removing a record from a one-to-many; other-side", function() { ok(foo1.get('barToOne').indexOf(bar1) >= 0, 'PRECOND - foo1.barToOne should contain bar1'); equals(bar1.get('fooToOne'), foo1, 'PRECOND - bar1.fooToOne should eq foo1'); checkAllClean(foo1, bar1); SC.run(function () { bar1.set('fooToOne', null); }); ok(foo1.get('barToOne').indexOf(bar1) < 0, 'foo1.barToOne should NOT contain bar1'); equals(bar1.get('fooToOne'), null, 'bar1.fooToOne should eq null'); equals(foo1.get('status'), SC.Record.READY_CLEAN, 'foo1.status should be READY_CLEAN'); equals(bar1.get('status'), SC.Record.READY_DIRTY, 'bar1.status should be READY_DIRTY'); }); test("add a record to a one-to-many; many-side", function() { ok(foo1.get('barToOne').indexOf(bar3) < 0, 'PRECOND - foo1.barToOne should NOT contain bar3'); equals(bar3.get('fooToOne'), null, 'PRECOND - bar3.fooToOne should eq null'); checkAllClean(foo1, bar1); SC.run(function () { foo1.get('barToOne').pushObject(bar3); }); ok(foo1.get('barToOne').indexOf(bar3) >= 0, 'foo1.barToOne should contain bar3'); equals(bar3.get('fooToOne'), foo1, 'bar3.fooToOne should eq foo1'); equals(foo1.get('status'), SC.Record.READY_CLEAN, 'foo1.status should be READY_CLEAN'); equals(bar3.get('status'), SC.Record.READY_DIRTY, 'bar3.status should be READY_DIRTY'); }); test("add a record to a one-to-many; one-side", function() { ok(foo1.get('barToOne').indexOf(bar3) < 0, 'PRECOND - foo1.barToOne should NOT contain bar3'); equals(bar3.get('fooToOne'), null, 'PRECOND - bar3.fooToOne should eq null'); checkAllClean(foo1, bar1); SC.run(function () { bar3.set('fooToOne', foo1); }); ok(foo1.get('barToOne').indexOf(bar3) >= 0, 'foo1.barToOne should contain bar3'); equals(bar3.get('fooToOne'), foo1, 'bar3.fooToOne should eq foo1'); equals(foo1.get('status'), SC.Record.READY_CLEAN, 'foo1.status should be READY_CLEAN'); equals(bar3.get('status'), SC.Record.READY_DIRTY, 'bar3.status should be READY_DIRTY'); });