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 CoreTest module */
  9 
 10 /** @class
 11 
 12   A test Suite defines a group of reusable unit tests that can be added to a 
 13   test plan at any time by calling the generate() method.  Suites are most
 14   useful for defining groups of tests that validate compliance with a mixin.
 15   You can then generate customized versions of the test suite for different
 16   types of objects to ensure that both the mixin and the object implementing
 17   the mixin use the API properly.
 18   
 19   ## Using a Suite
 20   
 21   To use a Suite, call the generate() method on the suite inside on of your
 22   unit test files.  This will generate new modules and tests in the suite
 23   and add them to your test plan.
 24   
 25   Usually you will need to customize the suite to apply to a specific object.
 26   You can supply these customizations through an attribute hash passed to the
 27   generate() method.  See the documentation on the specific test suite for
 28   information on the kind of customizations you may need to provide.
 29   
 30   ### Example
 31   
 32       // generates the SC.ArrayTestSuite tests for a built-in array.
 33       SC.ArrayTests.generate('Array', {
 34         newObject: function() { return []; }
 35       });
 36   
 37   ## Defining a Suite
 38   
 39   To define a test suite, simply call the extend() method, passing any 
 40   attributes you want to define on the suite along with this method.  You can
 41   then add functions that will define the test suite with the define() method.
 42   
 43   Functions you pass to define will have an instance of the test suite passed
 44   as their first parameter when invoked.
 45 
 46   ### Example 
 47   
 48       SC.ArrayTests = CoreTest.Suite.create("Verify SC.Array compliance", {
 49       
 50         // override to generate a new object that implements SC.Array
 51         newObject: function() { return null; }
 52       });
 53     
 54       SC.ArrayTests.define(function(T) {
 55         T.module("length tests");
 56       
 57         test("new length", function() {
 58           equals(T.object.get('length'), 0, 'array length');
 59         });
 60       
 61       });
 62   
 63   @since SproutCore 1.0
 64   
 65 */
 66 CoreTest.Suite = /** @scope CoreTest.Suite.prototype */ {
 67 
 68   /**
 69     Call this method to define a new test suite.  Pass one or more hashes of
 70     properties you want added to the new suite.  
 71     
 72     @param {Hash} attrs one or more attribute hashes
 73     @returns {CoreTest.Suite} subclass of suite.
 74   */
 75   create: function(desc, attrs) {
 76     var len = arguments.length,
 77         ret = CoreTest.beget(this),
 78         idx;
 79         
 80     // copy any attributes
 81     for(idx=1;idx<len;idx++) CoreTest.mixin(ret, arguments[idx]);
 82     
 83     if (desc) ret.basedesc = desc;
 84     
 85     // clone so that new definitions will be kept separate
 86     ret.definitions = ret.definitions.slice();
 87     
 88     return ret ;
 89   },
 90 
 91   /**
 92     Generate a new test suite instance, adding the suite definitions to the 
 93     current test plan.  Pass a description of the test suite as well as one or
 94     more attribute hashes to apply to the test plan.
 95     
 96     The description you add will be prefixed in front of the 'desc' property
 97     on the test plan itself.
 98     
 99     @param {String} desc suite description
100     @param {Hash} attrs one or more attribute hashes
101     @returns {CoreTest.Suite} suite instance
102   */
103   generate: function(desc, attrs) {
104     var len = arguments.length,
105         ret = CoreTest.beget(this),
106         idx, defs;
107         
108     // apply attributes - skip first argument b/c it is a string
109     for(idx=1;idx<len;idx++) CoreTest.mixin(ret, arguments[idx]);    
110     ret.subdesc = desc ;
111     
112     // invoke definitions
113     defs = ret.definitions ;
114     len = defs.length;
115     for(idx=0;idx<len;idx++) defs[idx].call(ret, ret);
116     
117     return ret ;
118   },
119   
120   /**
121     Adds the passed function to the array of definitions that will be invoked
122     when the suite is generated.
123     
124     The passed function should expect to have the TestSuite instance passed
125     as the first and only parameter.  The function should actually define 
126     a module and tests, which will be added to the test suite.
127     
128     @param {Function} func definition function
129     @returns {CoreTest.Suite} receiver
130   */
131   define: function(func) {
132     this.definitions.push(func);
133     return this ;
134   },
135   
136   /** 
137     Definition functions.  These are invoked in order when  you generate a 
138     suite to add unit tests and modules to the test plan.
139   */
140   definitions: [],
141   
142   /**
143     Generates a module description by merging the based description, sub 
144     description and the passed description.  This is usually used inside of 
145     a suite definition function.
146     
147     @param {String} str detailed description for this module
148     @returns {String} generated description
149   */
150   desc: function(str) {
151     return this.basedesc.fmt(this.subdesc, str);
152   },
153   
154   /**
155     The base description string.  This should accept two formatting options,
156     a sub description and a detailed description.  This is the description
157     set when you call extend()
158   */
159   basedesc: "%@ > %@",
160   
161   /**
162     Default setup method for use with modules.  This method will call the
163     newObject() method and set its return value on the object property of 
164     the receiver.
165   */
166   setup: function() {
167     this.object = this.newObject();
168   },
169   
170   /**
171     Default teardown method for use with modules.  This method will call the
172     destroyObject() method, passing the current object property on the 
173     receiver.  It will also clear the object property.
174   */
175   teardown: function() {
176     if (this.object) this.destroyObject(this.object);
177     this.object = null;
178   },
179   
180   /**
181     Default method to create a new object instance.  You will probably want
182     to override this method when you generate() a suite with a function that
183     can generate the type of object you want to test.
184     
185     @returns {Object} generated object
186   */
187   newObject: function() { return null; },
188   
189   /**
190     Default method to destroy a generated object instance after a test has 
191     completed.  If you override newObject() you can also override this method
192     to cleanup the object you just created.
193     
194     Default method does nothing.
195   */
196   destroyObject: function(obj) { 
197     // do nothing.
198   },
199   
200   /**
201     Generates a default module with the description you provide.  This is 
202     a convenience function for use inside of a definition function.  You could
203     do the same thing by calling:
204     
205         var T = this ;
206         module(T.desc(description), {
207           setup: function() { T.setup(); },
208           teardown: function() { T.teardown(); }
209         }
210     
211     @param {String} desc detailed description
212     @returns {CoreTest.Suite} receiver
213   */
214   module: function(desc) {
215     var T = this ;
216     module(T.desc(desc), {
217       setup: function() { T.setup(); },
218       teardown: function() { T.teardown(); }
219     });
220   }
221   
222 };
223