1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /*globals module test ok equals same SC */ 9 10 /** 11 Adds a new module of unit tests to verify that the passed object implements 12 the SC.Array interface. To generate, call the ArrayTests array with a 13 test descriptor. Any properties you pass will be applied to the ArrayTests 14 descendant created by the create method. 15 16 You should pass at least a newObject() method, which should return a new 17 instance of the object you want to have tested. You can also implement the 18 destroyObject() method, which should destroy a passed object. 19 20 SC.ArrayTests.generate("Array", { 21 newObject: function() { return []; } 22 }); 23 24 newObject must accept an optional array indicating the number of items 25 that should be in the array. You should initialize the the item with 26 that many items. The actual objects you add are up to you. 27 28 Unit tests themselves can be added by calling the define() method. The 29 function you pass will be invoked whenever the ArrayTests are generated. The 30 parameter passed will be the instance of ArrayTests you should work with. 31 32 SC.ArrayTests.define(function(T) { 33 T.module("length"); 34 35 test("verify length", function() { 36 var ary = T.newObject(); 37 equals(ary.get('length'), 0, 'should have 0 initial length'); 38 }); 39 } 40 */ 41 42 SC.TestSuite = /** @scope SC.TestSuite.prototype */ { 43 44 /** 45 Call this method to define a new test suite. Pass one or more hashes of 46 properties you want added to the new suite. 47 48 @param {Hash} attrs one or more attribute hashes 49 @returns {SC.TestSuite} subclass of suite. 50 */ 51 create: function(desc, attrs) { 52 var len = arguments.length, 53 ret = SC.beget(this), 54 idx; 55 56 // copy any attributes 57 for(idx=1;idx<len;idx++) SC.mixin(ret, arguments[idx]); 58 59 if (desc) ret.basedesc = desc; 60 61 // clone so that new definitions will be kept separate 62 ret.definitions = ret.definitions.slice(); 63 64 return ret ; 65 }, 66 67 /** 68 Generate a new test suite instance, adding the suite definitions to the 69 current test plan. Pass a description of the test suite as well as one or 70 more attribute hashes to apply to the test plan. 71 72 The description you add will be prefixed in front of the 'desc' property 73 on the test plan itself. 74 75 @param {String} desc suite description 76 @param {Hash} attrs one or more attribute hashes 77 @returns {SC.TestSuite} suite instance 78 */ 79 generate: function(desc, attrs) { 80 var len = arguments.length, 81 ret = SC.beget(this), 82 idx, defs; 83 84 // apply attributes - skip first argument b/c it is a string 85 for(idx=1;idx<len;idx++) SC.mixin(ret, arguments[idx]); 86 ret.subdesc = desc ; 87 88 // invoke definitions 89 defs = ret.definitions ; 90 len = defs.length; 91 for(idx=0;idx<len;idx++) defs[idx].call(ret, ret); 92 93 return ret ; 94 }, 95 96 /** 97 Adds the passed function to the array of definitions that will be invoked 98 when the suite is generated. 99 100 The passed function should expect to have the TestSuite instance passed 101 as the first and only parameter. The function should actually define 102 a module and tests, which will be added to the test suite. 103 104 @param {Function} func definition function 105 @returns {SC.TestSuite} receiver 106 */ 107 define: function(func) { 108 this.definitions.push(func); 109 return this ; 110 }, 111 112 /** 113 Definition functions. These are invoked in order when you generate a 114 suite to add unit tests and modules to the test plan. 115 */ 116 definitions: [], 117 118 /** 119 Generates a module description by merging the based description, sub 120 description and the passed description. This is usually used inside of 121 a suite definition function. 122 123 @param {String} str detailed description for this module 124 @returns {String} generated description 125 */ 126 desc: function(str) { 127 return this.basedesc.fmt(this.subdesc, str); 128 }, 129 130 /** 131 The base description string. This should accept two formatting options, 132 a sub description and a detailed description. This is the description 133 set when you call extend() 134 */ 135 basedesc: "%@ > %@", 136 137 /** 138 Default setup method for use with modules. This method will call the 139 newObject() method and set its return value on the object property of 140 the receiver. 141 */ 142 setup: function() { 143 this.object = this.newObject(); 144 }, 145 146 /** 147 Default teardown method for use with modules. This method will call the 148 destroyObject() method, passing the current object property on the 149 receiver. It will also clear the object property. 150 */ 151 teardown: function() { 152 if (this.object) this.destroyObject(this.object); 153 this.object = null; 154 }, 155 156 /** 157 Default method to create a new object instance. You will probably want 158 to override this method when you generate() a suite with a function that 159 can generate the type of object you want to test. 160 161 @returns {Object} generated object 162 */ 163 newObject: function() { return null; }, 164 165 /** 166 Default method to destroy a generated object instance after a test has 167 completed. If you override newObject() you can also override this method 168 to cleanup the object you just created. 169 170 Default method does nothing. 171 */ 172 destroyObject: function(obj) { 173 // do nothing. 174 }, 175 176 /** 177 Generates a default module with the description you provide. This is 178 a convenience function for use inside of a definition function. You could 179 do the same thing by calling: 180 181 var T = this ; 182 module(T.desc(description), { 183 setup: function() { T.setup(); }, 184 teardown: function() { T.teardown(); } 185 } 186 187 @param {String} desc detailed description 188 @returns {SC.TestSuite} receiver 189 */ 190 module: function(desc) { 191 var T = this ; 192 module(T.desc(desc), { 193 setup: function() { T.setup(); }, 194 teardown: function() { T.teardown(); } 195 }); 196 } 197 198 }; 199 200 SC.ArraySuite = SC.TestSuite.create("Verify SC.Array compliance: %@#%@", { 201 202 /** 203 Override to return a set of simple values such as numbers or strings. 204 Return null if your set does not support primitives. 205 */ 206 simple: function(amt) { 207 var ret = []; 208 if (amt === undefined) amt = 0; 209 while(--amt >= 0) ret[amt] = amt ; 210 return ret ; 211 }, 212 213 /** Override with the name of the key we should get/set on hashes */ 214 hashValueKey: 'foo', 215 216 /** 217 Override to return hashes of values if supported. Or return null. 218 */ 219 hashes: function(amt) { 220 var ret = []; 221 if (amt === undefined) amt = 0; 222 while(--amt >= 0) { 223 ret[amt] = {}; 224 ret[amt][this.hashValueKey] = amt ; 225 } 226 return ret ; 227 }, 228 229 /** Override with the name of the key we should get/set on objects */ 230 objectValueKey: "foo", 231 232 /** 233 Override to return observable objects if supported. Or return null. 234 */ 235 objects: function(amt) { 236 var ret = []; 237 if (amt === undefined) amt = 0; 238 while(--amt >= 0) { 239 var o = {}; 240 o[this.objectValueKey] = amt ; 241 ret[amt] = SC.Object.create(o); 242 } 243 return ret ; 244 }, 245 246 /** 247 Returns an array of content items in your preferred format. This will 248 be used whenever the test does not care about the specific object content. 249 */ 250 expected: function(amt) { 251 return this.simple(amt); 252 }, 253 254 /** 255 Example of how to implement newObject 256 */ 257 newObject: function(expected) { 258 if (!expected || SC.typeOf(expected) === SC.T_NUMBER) { 259 expected = this.expected(expected); 260 } 261 262 return expected.slice(); 263 }, 264 265 266 /** 267 Creates an observer object for use when tracking object modifications. 268 */ 269 observer: function(obj) { 270 return SC.Object.create({ 271 272 // .......................................................... 273 // NORMAL OBSERVER TESTING 274 // 275 276 observer: function(target, key, value) { 277 this.notified[key] = true ; 278 this.notifiedValue[key] = value ; 279 }, 280 281 resetObservers: function() { 282 this.notified = {} ; 283 this.notifiedValue = {} ; 284 }, 285 286 observe: function() { 287 var keys = SC.$A(arguments) ; 288 var loc = keys.length ; 289 while(--loc >= 0) { 290 obj.addObserver(keys[loc], this, this.observer) ; 291 } 292 return this ; 293 }, 294 295 didNotify: function(key) { 296 return !!this.notified[key] ; 297 }, 298 299 init: function() { 300 sc_super() ; 301 this.resetObservers() ; 302 }, 303 304 // .......................................................... 305 // RANGE OBSERVER TESTING 306 // 307 308 callCount: 0, 309 310 // call afterward to verify 311 expectRangeChange: function(source, object, key, indexes, context) { 312 equals(this.callCount, 1, 'expects one callback'); 313 314 if (source !== undefined && source !== NO) { 315 ok(this.source, source, 'source should equal array'); 316 } 317 318 if (object !== undefined && object !== NO) { 319 equals(this.object, object, 'object'); 320 } 321 322 if (key !== undefined && key !== NO) { 323 equals(this.key, key, 'key'); 324 } 325 326 if (indexes !== undefined && indexes !== NO) { 327 if (indexes.isIndexSet) { 328 ok(this.indexes && this.indexes.isIndexSet, 'indexes should be index set'); 329 ok(indexes.isEqual(this.indexes), 'indexes should match %@ (actual: %@)'.fmt(indexes, this.indexes)); 330 } else equals(this.indexes, indexes, 'indexes'); 331 } 332 333 if (context !== undefined && context !== NO) { 334 equals(this.context, context, 'context should match'); 335 } 336 337 }, 338 339 rangeDidChange: function(source, object, key, indexes, context) { 340 this.callCount++ ; 341 this.source = source ; 342 this.object = object ; 343 this.key = key ; 344 345 // clone this because the index set may be reused after this callback 346 // runs. 347 this.indexes = (indexes && indexes.isIndexSet) ? indexes.clone() : indexes; 348 this.context = context ; 349 } 350 351 }); 352 }, 353 354 /** 355 Verifies that the passed object matches the passed array. 356 */ 357 validateAfter: function(obj, after, observer, lengthDidChange, enumerableDidChange) { 358 var loc = after.length; 359 equals(obj.get('length'), loc, 'length should update (%@)'.fmt(obj)) ; 360 while(--loc >= 0) { 361 equals(obj.objectAt(loc), after[loc], 'objectAt(%@)'.fmt(loc)) ; 362 } 363 364 // note: we only test that the length notification happens when we expect 365 // it. If we don't expect a length notification, it is OK for a class 366 // to trigger a change anyway so we don't check for this case. 367 if (enumerableDidChange !== NO) { 368 equals(observer.didNotify("[]"), YES, 'should notify []') ; 369 } 370 371 if (lengthDidChange) { 372 equals(observer.didNotify('length'), YES, 'should notify length change'); 373 } 374 } 375 376 }); 377 378 // Simple verification of length 379 SC.ArraySuite.define(function(T) { 380 T.module("length"); 381 382 test("should return 0 on empty array", function() { 383 equals(T.object.get('length'), 0, 'should have empty length'); 384 }); 385 386 test("should return array length", function() { 387 var obj = T.newObject(3); 388 equals(obj.get('length'), 3, 'should return length'); 389 }); 390 391 }); 392