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