1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Apple Inc. and contributors.
  4 // License:   Licensed under MIT license (see license.js)
  5 // ==========================================================================
  6 /*globals module, ok, equals, same, test, MyApp */
  7 
  8 // test core array-mapping methods for ManyArray
  9 var store, storeKey, storeId, rec, storeIds, recs, arrayRec;
 10 module("SC.ManyArray core methods", {
 11   setup: function() {
 12 
 13     // setup dummy app and store
 14     MyApp = SC.Object.create({
 15       store: SC.Store.create()
 16     });
 17 
 18     // setup a dummy model
 19     MyApp.Foo = SC.Record.extend({});
 20 
 21     SC.RunLoop.begin();
 22 
 23     // load some data
 24     storeIds = [1,2,3,4];
 25     MyApp.store.loadRecords(MyApp.Foo, [
 26       { guid: 1, firstName: "John", lastName: "Doe", age: 32 },
 27       { guid: 2, firstName: "Jane", lastName: "Doe", age: 30 },
 28       { guid: 3, firstName: "Emily", lastName: "Parker", age: 7 },
 29       { guid: 4, firstName: "Johnny", lastName: "Cash", age: 17 },
 30       { guid: 50, firstName: "Holder", fooMany: storeIds }
 31     ]);
 32 
 33     storeKey = MyApp.store.storeKeyFor(MyApp.Foo, 1);
 34 
 35     // get record
 36     rec = MyApp.store.materializeRecord(storeKey);
 37     storeId = rec.get('id');
 38 
 39     // get many array.
 40     arrayRec = MyApp.store.materializeRecord(MyApp.store.storeKeyFor(MyApp.Foo, 50));
 41 
 42     recs = SC.ManyArray.create({
 43       record: arrayRec,
 44       propertyName: "fooMany",
 45       recordType: MyApp.Foo,
 46       isEditable: YES
 47     });
 48     arrayRec.relationships = [recs];
 49   },
 50 
 51   teardown: function() {
 52     SC.RunLoop.end();
 53   }
 54 });
 55 
 56 // ..........................................................
 57 // LENGTH
 58 //
 59 
 60 test("should pass through length", function() {
 61   equals(recs.get('length'), storeIds.length, 'rec should pass through length');
 62 });
 63 
 64 test("changing storeIds length should change length of rec array also", function() {
 65 
 66   var oldlen = recs.get('length');
 67 
 68   storeIds.pushObject(SC.Store.generateStoreKey()); // change length
 69 
 70   ok(storeIds.length > oldlen, 'precond - storeKeys.length should have changed');
 71   equals(recs.get('length'), storeIds.length, 'rec should pass through length');
 72 });
 73 
 74 // ..........................................................
 75 // objectAt
 76 //
 77 
 78 test("should materialize record for object", function() {
 79   equals(storeIds[0], storeId, 'precond - storeIds[0] should be storeId');
 80   equals(recs.objectAt(0), rec, 'recs.objectAt(0) should materialize record');
 81 });
 82 
 83 test("reading past end of array length should return undefined", function() {
 84   equals(recs.objectAt(2000), undefined, 'recs.objectAt(2000) should be undefined');
 85 });
 86 
 87 /** Changed in Version 1.11
 88   Previously, you could modify the datahash's storeIds array directly because it was observed,
 89   however the act of observing the array adds the observable properties to it, which
 90   in effect alters the array. This is now prohibited and all access must be done through the
 91   proper KVO channels on the ManyArray.
 92   */
 93 test("modifying the underlying storeId should *not* change the returned materialized record", function() {
 94 
 95   // read record once to make it materialized
 96   var rec2 = recs.objectAt(1);
 97 
 98   equals(recs.objectAt(0), rec, 'recs.objectAt(0) should materialize record');
 99 
100   // create a new record.
101   var newRec = MyApp.store.createRecord(MyApp.Foo, { guid: 5, firstName: "Fred" });
102   var storeId2 = newRec.get('id');
103 
104   // add to beginning of storeKey array
105   storeIds.unshiftObject(storeId2);
106   recs.recordPropertyDidChange();
107 
108   equals(recs.get('length'), 5, 'should now have length of 5');
109   equals(recs.objectAt(0), rec, 'objectAt(0) should return the old record still');
110   equals(recs.objectAt(1), rec2, 'objectAt(1) should return the old second record still');
111 });
112 
113 test("reading a record not loaded in store should trigger retrieveRecord", function() {
114   var callCount = 0;
115 
116   // patch up store to record a call and to make it look like data is not
117   // loaded.
118 
119   MyApp.store.removeDataHash(storeKey, SC.Record.EMPTY);
120   MyApp.store.retrieveRecord = function() { callCount++; };
121 
122   var rec = recs.objectAt(0);
123   equals(MyApp.store.readStatus(rec), SC.Record.EMPTY, 'precond - storeKey must not be loaded');
124 
125   equals(callCount, 1, 'store.retrieveRecord() should have been called');
126 });
127 
128 // ..........................................................
129 // replace()
130 //
131 
132 test("adding a record to the ManyArray should pass through storeIds", function() {
133 
134   // read record once to make it materialized
135   equals(recs.objectAt(0), rec, 'recs.objectAt(0) should materialize record');
136 
137   // create a new record.
138   var rec2 = MyApp.store.createRecord(MyApp.Foo, { guid: 5, firstName: "rec2" });
139   var storeId2 = rec2.get('id');
140 
141   // add record to beginning of record array
142   recs.unshiftObject(rec2);
143 
144   // verify record array
145   equals(recs.get('length'), 5, 'should now have length of 2');
146   equals(recs.objectAt(0), rec2, 'recs.objectAt(0) should return new record');
147   equals(recs.objectAt(1), rec, 'recs.objectAt(1) should return old record');
148 
149   // verify storeKeys
150   storeIds = arrayRec.readAttribute('fooMany'); // array might have changed
151   equals(storeIds.objectAt(0), storeId2, 'storeKeys[0] should return new storeKey');
152   equals(storeIds.objectAt(1), storeId, 'storeKeys[1] should return old storeKey');
153 });
154 
155 // ..........................................................
156 // Property Observing
157 //
158 
159 /** Changed in Version 1.11
160   Previously, you could modify the datahash's storeIds array directly because it was observed,
161   however the act of observing the array adds the observable properties to it, which
162   in effect alters the array. This is now prohibited and all access must be done through the
163   proper KVO channels on the ManyArray.
164   */
165 test("changing the underlying storeIds should *not* notify observers of records", function() {
166 
167   // setup observer
168   var obj = SC.Object.create({
169     cnt: 0,
170     observer: function() { this.cnt++; }
171   });
172   recs.addObserver('[]', obj, obj.observer);
173 
174   // now modify storeKeys
175   storeIds.pushObject(5);
176   equals(obj.cnt, 0, 'observer should not have fired after changing storeKeys directly');
177 });
178 
179 /** Changed in Version 1.11
180   Previously, you could modify the datahash's storeIds array directly because it was observed,
181   however the act of observing the array adds the observable properties to it, which
182   in effect alters the array. This is now prohibited and all access must be done through the
183   proper KVO channels on the ManyArray.
184   */
185 test("swapping storeIds array should change ManyArray and observers", function() {
186 
187   // setup alternate storeKeys
188   var rec2 = MyApp.store.createRecord(MyApp.Foo, { guid: 5, firstName: "rec2" });
189   var storeId2 = rec2.get('id');
190   var storeIds2 = [storeId2];
191 
192   // setup observer
193   var obj = SC.Object.create({
194     cnt: 0,
195     observer: function() {
196       this.cnt++;
197     }
198   });
199   recs.addObserver('[]', obj, obj.observer);
200 
201   // read record once to make it materialized
202   equals(recs.objectAt(0), rec, 'recs.objectAt(0) should materialize record');
203 
204   // now swap storeKeys
205   obj.cnt = 0 ;
206   arrayRec.writeAttribute('fooMany', storeIds2);
207 
208   SC.RunLoop.begin();
209   SC.RunLoop.end();
210 
211   // verify observer fired and record changed
212   equals(obj.cnt, 1, 'observer should have fired after swap');
213   equals(recs.get('length'), 1, 'should reflect new length');
214   equals(recs.objectAt(0), rec2, 'recs.objectAt(0) should return new rec');
215 
216   // modify storeKey2, make sure observer fires and content changes
217   obj.cnt = 0;
218   storeIds2.unshiftObject(storeId);
219   equals(obj.cnt, 0, 'observer should not have fired after direct edit');
220   equals(recs.get('length'), 2, 'should reflect new length still: DANGER!');
221   equals(recs.objectAt(0), rec2, 'recs.objectAt(0) should return old rec still');
222 
223 });
224 
225 test("reduced properties", function() {
226   equals(recs.get('@sum(age)'), 32+30+7+17, 'sum reducer should return the correct value');
227   equals(recs.get('@max(age)'), 32, 'max reducer should return the correct value');
228   equals(recs.get('@min(age)'), 7, 'min reducer should return the correct value');
229   equals(recs.get('@average(age)'), (32+30+7+17)/4.0, 'average reducer should return the correct value');
230 });
231 
232 test("Test that _findInsertionLocation returns the correct location.", function () {
233   var location,
234     newRec,
235     sortByFirstName = function (a, b) {
236       if (a.get('firstName') == b.get('firstName')) return 0;
237       else if (a.get('firstName') < b.get('firstName')) return -1;
238       else return 1;
239     };
240 
241   // Order the many array manually by firstName.
242   arrayRec.set('fooMany', [3,2,1,4]);
243   recs._records = null;
244   recs.arrayContentDidChange(0, 4, 4);
245 
246   // Check the insertion location of a record that should appear first.
247   newRec = SC.Object.create({ guid: 5, firstName: "Adam", lastName: "Doe", age: 15 });
248   location = recs._findInsertionLocation(newRec, 0, recs.get('length') - 1, sortByFirstName);
249 
250   equals(location, 0, "The insertion location should be");
251 
252   // Check the insertion location of a record that should appear in the middle.
253   newRec = SC.Object.create({ guid: 5, firstName: "Farmer", lastName: "Doe", age: 95 });
254   location = recs._findInsertionLocation(newRec, 0, recs.get('length') - 1, sortByFirstName);
255 
256   equals(location, 1, "The insertion location should be");
257 
258   newRec = SC.Object.create({ guid: 5, firstName: "Jen", lastName: "Doe", age: 95 });
259   location = recs._findInsertionLocation(newRec, 0, recs.get('length') - 1, sortByFirstName);
260 
261   equals(location, 2, "The insertion location should be");
262 
263   newRec = SC.Object.create({ guid: 5, firstName: "Johnny", lastName: "Doe", age: 95 });
264   location = recs._findInsertionLocation(newRec, 0, recs.get('length') - 1, sortByFirstName);
265 
266   equals(location, 3, "The insertion location should be");
267 
268   // Check the insertion location of a record that should appear last.
269   newRec = SC.Object.create({ guid: 5, firstName: "Zues", lastName: "Doe", age: 95 });
270   location = recs._findInsertionLocation(newRec, 0, recs.get('length') - 1, sortByFirstName);
271 
272   equals(location, 4, "The insertion location should be");
273 });
274 
275 // ..........................................................
276 // New records
277 //
278 
279 test("Test new record support. INCOMPLETE.", function () {
280   var newRec = MyApp.store.createRecord(MyApp.Foo, { firstName: "Adam", lastName: "Doe", age: 15 }),
281     holder = MyApp.store.find(MyApp.Foo, 50);
282 
283   recs.set('supportNewRecords', false);
284   try {
285     recs.pushObject(newRec);
286     ok(false, "Should not be able to push a record without an id without supportNewRecords.");
287   } catch (ex) {
288     ok(true, "Should not be able to push a record without an id without supportNewRecords.");
289   }
290 
291   recs.set('supportNewRecords', true);
292   try {
293     recs.pushObject(newRec);
294     ok(true, "Should be able to push a record without an id normally.");
295   } catch (ex) {
296     ok(false, "Should be able to push a record without an id normally.");
297   }
298 
299   equals(newRec.get('id'), undefined, "The transient record should still have an undefined id.");
300   equals(recs.objectAt(4), newRec, "The transient record should be accessible in the many array.");
301   //equals(holder.get('status'), SC.Record.READY_CLEAN, "The record should not be dirtied when the transient record is added.");
302   warn("The record should not be dirtied when the transient record is added. Not yet implemented.");
303 
304   SC.run(function () {
305     newRec.set('id', 200);
306   });
307 
308   equals(newRec.get('id'), 200, "The post-transient record should have an id of 200.");
309   //equals(holder.get('status'), SC.Record.READY_DIRTY, "The record should be dirtied when the relationship is actually updated.");
310   warn("The record should be dirtied when the relationship is actually updated. Not yet implemented.");
311 
312 });
313