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