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 Q$ */
  9 
 10 var QUNIT_BREAK_ON_TEST_FAIL = false;
 11 
 12 /** @class
 13 
 14   A test plan contains a set of functions that will be executed in order.  The
 15   results will be recorded into a results hash as well as calling a delegate.
 16 
 17   When you define tests and modules, you are adding to the active test plan.
 18   The test plan is then run when the page has finished loading.
 19 
 20   Normally you will not need to work with a test plan directly, though if you
 21   are writing a test runner application that needs to monitor test progress
 22   you may write a delegate to talk to the test plan.
 23 
 24   The CoreTest.Plan.fn hash contains functions that will be made global via
 25   wrapper methods.  The methods must accept a Plan object as their first
 26   parameter.
 27 
 28   ## Results
 29 
 30   The results hash contains a summary of the results of running the test
 31   plan.  It includes the following properties:
 32 
 33    - *assertions* -- the total number of assertions
 34    - *tests* -- the total number of tests
 35    - *passed* -- number of assertions that passed
 36    - *failed* -- number of assertions that failed
 37    - *errors* -- number of assertions with errors
 38    - *warnings* -- number of assertions with warnings
 39 
 40   You can also consult the log property, which contains an array of hashes -
 41   one for each assertion - with the following properties:
 42 
 43    - *module* -- module descriptions
 44    - *test* -- test description
 45    - *message* -- assertion description
 46    - *result* -- CoreTest.OK, CoreTest.FAILED, CoreTest.ERROR, CoreTest.WARN
 47 
 48   @since SproutCore 1.0
 49 */
 50 CoreTest.Plan = {
 51 
 52   /**
 53     Define a new test plan instance.  Optionally pass attributes to apply
 54     to the new plan object.  Usually you will call this without arguments.
 55 
 56     @param {Hash} attrs plan arguments
 57     @returns {CoreTest.Plan} new instance/subclass
 58   */
 59   create: function(attrs) {
 60     var len = arguments.length,
 61         ret = CoreTest.beget(this),
 62         idx;
 63     for(idx=0;idx<len;idx++) CoreTest.mixin(ret, attrs);
 64     ret.queue = ret.queue.slice(); // want an independent queue
 65     return ret ;
 66   },
 67 
 68   // ..........................................................
 69   // RUNNING
 70   //
 71 
 72   /** @private - array of functions to execute in order. */
 73   queue: [],
 74 
 75   /**
 76     If true then the test plan is currently running and items in the queue
 77     will execute in order.
 78 
 79     @type {Boolean}
 80   */
 81   isRunning: false,
 82 
 83   /**
 84     Primitive used to add callbacks to the test plan queue.  Usually you will
 85     not want to call this method directly but instead use the module() or
 86     test() methods.
 87 
 88     @returns {CoreTest.Plan} receiver
 89   */
 90   synchronize: function synchronize(callback) {
 91     this.queue.push(callback);
 92     if (this.isRunning) this.process(); // run queue
 93     return this;
 94   },
 95 
 96   /**
 97     Processes items in the queue as long as isRunning remained true.  When
 98     no further items are left in the queue, calls finish().  Usually you will
 99     not call this method directly.  Instead call run().
100 
101     @returns {CoreTest.Plan} receiver
102   */
103   process: function process() {
104     while(this.queue.length && this.isRunning) {
105       this.queue.shift().call(this);
106     }
107     return this ;
108   },
109 
110   /**
111     Begins running the test plan after a slight delay to avoid interrupting
112     any current callbacks.
113 
114     @returns {CoreTest.Plan} receiver
115   */
116   start: function() {
117     var plan = this ;
118     setTimeout(function() {
119       if (plan.timeout) clearTimeout(plan.timeout);
120       plan.timeout = null;
121       plan.isRunning = true;
122       plan.process();
123     }, 13);
124     return this ;
125   },
126 
127   /**
128     Stops the test plan from running any further.  If you pass a timeout,
129     it will raise an exception if the test plan does not begin executing
130     with the allotted timeout.
131 
132     @param {Number} timeout optional timeout in msec
133     @returns {CoreTest.Plan} receiver
134   */
135   stop: function(timeout) {
136     this.isRunning = false ;
137 
138     if (this.timeout) clearTimeout(this.timeout);
139     if (timeout) {
140       var plan = this;
141       this.timeout = setTimeout(function() {
142         plan.fail("Test timed out").start();
143       }, timeout);
144     } else this.timeout = null ;
145     return this ;
146   },
147 
148   /**
149     Force the test plan to take a break.  Avoids slow script warnings.  This
150     is called automatically after each test completes.
151   */
152   pause: function() {
153     if (this.isRunning) {
154       var del = this.delegate;
155       if (del && del.planDidPause) del.planDidPause(this);
156 
157       this.isRunning = false ;
158       this.start();
159     }
160     return this ;
161   },
162 
163   /**
164     Initiates running the tests for the first time.  This will add an item
165     to the queue to call finish() on the plan when the run completes.
166 
167     @returns {CoreTest.Plan} receiver
168   */
169   run: function() {
170     this.isRunning = true;
171     this.prepare();
172 
173     // initialize new results
174     this.results = {
175       start: new Date().getTime(),
176       finish: null,
177       runtime: 0,
178       tests: 0,
179       total: 0,
180       passed: 0,
181       failed: 0,
182       errors: 0,
183       warnings: 0,
184       assertions: []
185     };
186 
187     // add item to queue to finish running the test plan when finished.
188     this.begin().synchronize(this.finish).process();
189 
190     return this ;
191   },
192 
193   /**
194     Called when the test plan begins running.  This method will notify the
195     delegate.  You will not normally call this method directly.
196 
197     @returns {CoreTest.Plan} receiver
198   */
199   begin: function() {
200     var del = this.delegate;
201     if (del && del.planDidBegin) del.planDidBegin(this);
202     return this ;
203   },
204 
205   /**
206     When the test plan finishes running, this method will be called to notify
207     the delegate that the plan as finished.
208 
209     @returns {CoreTest.Plan} receiver
210   */
211   finish: function() {
212     var r   = this.results,
213         del = this.delegate;
214 
215     r.finish = new Date().getTime();
216     r.runtime = r.finish - r.start;
217 
218     if (del && del.planDidFinish) del.planDidFinish(this, r);
219     return this ;
220   },
221 
222   /**
223     Sets the current module information.  This will be used when a test is
224     added under the module.
225 
226     @returns {CoreTest.Plan} receiver
227   */
228   module: function(desc, lifecycle) {
229     if (typeof SC !== 'undefined' && SC.filename) {
230       desc = SC.filename.replace(/^.+?\/current\/tests\//,'') + '\n' + desc;
231     }
232 
233     this.currentModule = desc;
234 
235     if (!lifecycle) lifecycle = {};
236     this.setup(lifecycle.setup).teardown(lifecycle.teardown);
237 
238     return this ;
239   },
240 
241   /**
242     Sets the current setup method.
243 
244     @returns {CoreTest.Plan} receiver
245   */
246   setup: function(func) {
247     this.currentSetup = func || CoreTest.K;
248     return this;
249   },
250 
251   /**
252     Sets the current teardown method
253 
254     @returns {CoreTest.Plan} receiver
255   */
256   teardown: function teardown(func) {
257     this.currentTeardown = func || CoreTest.K ;
258     return this;
259   },
260 
261   now: function() { return new Date().getTime(); },
262 
263   /**
264     Generates a unit test, adding it to the test plan.
265   */
266   test: function test(desc, func) {
267 
268     if (!this.enabled(this.currentModule, desc)) return this; // skip
269 
270     // base prototype describing test
271     var working = {
272       module: this.currentModule,
273       test: desc,
274       expected: 0,
275       assertions: []
276     };
277 
278     var msg;
279     var name = desc ;
280     if (this.currentModule) name = this.currentModule + " module: " + name;
281 
282     var setup = this.currentSetup || CoreTest.K;
283     var teardown = this.currentTeardown || CoreTest.K;
284 
285     // add setup to queue
286     this.synchronize(function() {
287 
288       // save main fixture...
289       var mainEl = document.getElementById('main');
290       this.fixture = mainEl ? mainEl.innerHTML : '';
291       mainEl = null;
292 
293       this.working = working;
294 
295       try {
296         working.total_begin = working.setup_begin = this.now();
297         setup.call(this);
298         working.setup_end = this.now();
299       } catch(e) {
300         msg = (e && e.toString) ? e.toString() : "(unknown error)";
301         this.error("Setup exception on " + name + ": " + msg);
302       }
303     });
304 
305     // now actually invoke test
306     this.synchronize(function() {
307       if (!func) {
308         this.warn("Test not yet implemented: " + name);
309       } else {
310         try {
311           if (CoreTest.trace) console.log("run: " + name);
312           this.working.test_begin = this.now();
313           func.call(this);
314           this.working.test_end = this.now();
315         } catch(e) {
316           msg = (e && e.toString) ? e.toString() : "(unknown error)";
317           this.error("Died on test #" + (this.working.assertions.length + 1) + ": " + msg);
318         }
319       }
320     });
321 
322     // cleanup
323     this.synchronize(function() {
324       try {
325         this.working.teardown_begin = this.now();
326         teardown.call(this);
327         this.working.teardown_end = this.now();
328       } catch(e) {
329         msg = (e && e.toString) ? e.toString() : "(unknown error)";
330         this.error("Teardown exception on " + name + ": " + msg);
331       }
332     });
333 
334     // finally, reset and report result
335     this.synchronize(function() {
336 
337       if (this.reset) {
338         try {
339           this.working.reset_begin = this.now();
340           this.reset();
341           this.working.total_end = this.working.reset_end = this.now();
342         } catch(ex) {
343           msg = (ex && ex.toString) ? ex.toString() : "(unknown error)";
344           this.error("Reset exception on " + name + ": " + msg) ;
345         }
346       }
347 
348       // check for expected assertions
349       var w = this.working,
350           exp = w.expected,
351           len = w.assertions.length;
352 
353       if (exp && exp !== len) {
354         this.fail("Expected " + exp + " assertions, but " + len + " were run");
355       }
356 
357       // finally, record result
358       this.working = null;
359       this.record(w.module, w.test, w.assertions, w);
360 
361       if (!this.pauseTime) {
362         this.pauseTime = new Date().getTime();
363       } else {
364         var now = new Date().getTime();
365         if ((now - this.pauseTime) > 250) {
366           this.pause();
367           this.pauseTime = now ;
368         }
369       }
370 
371     });
372   },
373 
374   clearHtmlbody: function(){
375     var body = Q$('body')[0];
376 
377     // first, find the first element with id 'htmlbody-begin'  if exists,
378     // remove everything after that to reset...
379     var begin = Q$('body #htmlbody-begin')[0];
380     if (!begin) {
381       begin = Q$('<div id="htmlbody-begin"></div>')[0];
382       body.appendChild(begin);
383     } else {
384       while(begin.nextSibling) body.removeChild(begin.nextSibling);
385     }
386     begin = null;
387   },
388 
389   /**
390     Converts the passed string into HTML and then appends it to the main body
391     element.  This is a useful way to automatically load fixture HTML into the
392     main page.
393   */
394   htmlbody: function htmlbody(string) {
395     var html = Q$(string) ;
396     var body = Q$('body')[0];
397 
398     this.clearHtmlbody();
399 
400     // now append new content
401     html.each(function() { body.appendChild(this); });
402   },
403 
404   /**
405     Records the results of a test.  This will add the results to the log
406     and notify the delegate.  The passed assertions array should contain
407     hashes with the result and message.
408   */
409   record: function(module, test, assertions, timings) {
410     var r   = this.results,
411         len = assertions.length,
412         del = this.delegate,
413         idx, cur;
414 
415     r.tests++;
416     for(idx=0;idx<len;idx++) {
417       cur = assertions[idx];
418       cur.module = module;
419       cur.test = test ;
420 
421       r.total++;
422       r[cur.result]++;
423       r.assertions.push(cur);
424     }
425 
426     if (del && del.planDidRecord) {
427       del.planDidRecord(this, module, test, assertions, timings) ;
428     }
429 
430   },
431 
432   /**
433     Universal method can be called to reset the global state of the
434     application for each test.  The default implementation will reset any
435     saved fixture.
436   */
437   reset: function() {
438     if (this.fixture) {
439       var mainEl = document.getElementById('main');
440       if (mainEl) mainEl.innerHTML = this.fixture;
441       mainEl = null;
442     }
443     return this ;
444   },
445 
446   /**
447     Can be used to decide if a particular test should be enabled or not.
448     Current implementation allows a test to run.
449 
450     @returns {Boolean}
451   */
452   enabled: function(moduleName, testName) {
453     return true;
454   },
455 
456   // ..........................................................
457   // MATCHERS
458   //
459 
460   /**
461     Called by a matcher to record that a test has passed.  Requires a working
462     test property.
463   */
464   pass: function(msg) {
465     var w = this.working ;
466     if (!w) throw new Error("pass("+msg+") called outside of a working test");
467     w.assertions.push({ message: msg, result: CoreTest.OK });
468     return this ;
469   },
470 
471   /**
472     Called by a matcher to record that a test has failed.  Requires a working
473     test property.
474   */
475   fail: function(msg) {
476     var w = this.working ;
477     if (!w) throw new Error("fail("+msg+") called outside of a working test");
478     w.assertions.push({ message: msg, result: CoreTest.FAIL });
479     return this ;
480   },
481 
482   /**
483     Called by a matcher to record that a test issued a warning.  Requires a
484     working test property.
485   */
486   warn: function(msg) {
487     var w = this.working ;
488     if (!w) throw new Error("warn("+msg+") called outside of a working test");
489     w.assertions.push({ message: msg, result: CoreTest.WARN });
490     return this ;
491   },
492 
493   /**
494     Called by a matcher to record that a test had an error.  Requires a
495     working test property.
496   */
497   error: function(msg, e) {
498     var w = this.working ;
499     if (!w) throw new Error("error("+msg+") called outside of a working test");
500 
501     if(e && typeof console != "undefined" && console.error && console.warn ) {
502       console.error(msg);
503       console.error(e);
504     }
505 
506     w.assertions.push({ message: msg, result: CoreTest.ERROR });
507     return this ;
508   },
509 
510   /**
511     Any methods added to this hash will be made global just before the first
512     test is run.  You can add new methods to this hash to use them in unit
513     tests.  "this" will always be the test plan.
514   */
515   fn: {
516 
517     /**
518       Primitive will pass or fail the test based on the first boolean.  If you
519       pass an actual and expected value, then this will automatically log the
520       actual and expected values.  Otherwise, it will expect the message to
521       be passed as the second argument.
522 
523       @param {Boolean} pass true if pass
524       @param {Object} actual optional actual
525       @param {Object} expected optional expected
526       @param {String} msg optional message
527       @returns {CoreTest.Plan} receiver
528     */
529     ok: function ok(pass, actual, expected, msg) {
530       if (msg === undefined) {
531         msg = actual ;
532         if (!msg) msg = pass ? "OK" : "failed";
533       } else {
534         if (!msg) msg = pass ? "OK" : "failed";
535         if (pass) {
536           msg = msg + ": " + CoreTest.dump(expected) ;
537         } else {
538           msg = msg + ", expected: " + CoreTest.dump(expected) + " result: " + CoreTest.dump(actual);
539         }
540       }
541 
542       if (QUNIT_BREAK_ON_TEST_FAIL & !pass) {
543         SC.throw(msg);
544       }
545 
546       return !!pass ? this.pass(msg) : this.fail(msg);
547     },
548 
549     /**
550       Primitive performs a basic equality test on the passed values.  Prints
551       out both actual and expected values.
552 
553       Preferred to ok(actual === expected, message);
554 
555       @param {Object} actual tested object
556       @param {Object} expected expected value
557       @param {String} msg optional message
558       @returns {CoreTest.Plan} receiver
559     */
560     equals: function equals(actual, expected, msg) {
561       if (msg === undefined) msg = null; // make sure ok logs properly
562       return this.ok(actual == expected, actual, expected, msg);
563     },
564 
565     /**
566       Expects the passed function call to throw an exception of the given
567       type. If you pass null or Error for the expected exception, this will
568       pass if any error is received.  If you pass a string, this will check
569       message property of the exception.
570 
571       @param {Function} callback the function to execute
572       @param {Error} expected optional, the expected error
573       @param {String} a description
574       @returns {CoreTest.Plan} receiver
575     */
576     should_throw: function should_throw(callback, expected, msg) {
577       var actual = false ;
578 
579       try {
580         callback();
581       } catch(e) {
582         actual = (typeof expected === "string") ? e.message : e;
583       }
584 
585       if (expected===false) {
586         ok(actual===false, CoreTest.fmt("%@ expected no exception, actual %@", msg, actual));
587       } else if (expected===Error || expected===null || expected===true) {
588         ok(!!actual, CoreTest.fmt("%@ expected exception, actual %@", msg, actual));
589       } else {
590         equals(actual, expected, msg);
591       }
592     },
593 
594     /**
595       Specify the number of expected assertions to gaurantee that a failed
596       test (no assertions are run at all) don't slip through
597 
598       @returns {CoreTest.Plan} receiver
599     */
600     expect: function expect(asserts) {
601       this.working.expected = asserts;
602     },
603 
604     /**
605       Verifies that two objects are actually the same.  This method will do
606       a deep compare instead of a simple equivalence test.  You should use
607       this instead of equals() when you expect the two object to be different
608       instances but to have the same content.
609 
610       @param {Object} value tested object
611       @param {Object} actual expected value
612       @param {String} msg optional message
613       @returns {CoreTest.Plan} receiver
614     */
615     same: function(actual, expected, msg) {
616       if (msg === undefined) msg = null ; // make sure ok logs properly
617       return this.ok(CoreTest.equiv(actual, expected), actual, expected, msg);
618     },
619 
620     /**
621       Logs a warning. Useful for clearly marking assertions that will need to be revisited
622       after future development. Warnings are highlighted prominently in the test runner, but
623       do not mark the test suite as failed.
624 
625       @param {String} msg the warning message
626       @returns {CoreTest.Plan} receiver
627     */
628     warn: function(msg) {
629       if (msg === undefined) msg = null;
630       return this.warn(msg);
631     },
632 
633     /**
634       Stops the current tests from running.  An optional timeout will
635       automatically fail the test if it does not restart within the specified
636       period of time.
637 
638       @param {Number} timeout timeout in msec
639       @returns {CoreTest.Plan} receiver
640     */
641     stop: function(timeout) {
642       return this.stop(timeout);
643     },
644 
645     /**
646       Restarts tests running.  Use this to begin tests after you stop tests.
647 
648       @returns {CoreTest.Plan} receiver
649     */
650     start: function() {
651       return this.start();
652     },
653 
654     reset: function() {
655       return this.reset();
656     }
657 
658   },
659 
660   /**
661     Exports the comparison functions into the global namespace.  This will
662     allow you to call these methods from within testing functions.  This
663     method is called automatically just before the first test is run.
664 
665     @returns {CoreTest.Plan} receiver
666   */
667   prepare: function() {
668     var fn   = this.fn,
669         plan = this,
670         key, func;
671 
672     for(key in fn) {
673       if (!fn.hasOwnProperty(key)) continue ;
674       func = fn[key];
675       if (typeof func !== "function") continue ;
676       window[key] = this._bind(func);
677       if (!plan[key]) plan[key] = func;
678     }
679     return this ;
680   },
681 
682   _bind: function(func) {
683     var plan = this;
684     return function() { return func.apply(plan, arguments); };
685   }
686 
687 };
688 
689 // ..........................................................
690 // EXPORT BASIC API
691 //
692 
693 CoreTest.defaultPlan = function defaultPlan() {
694   var plan = CoreTest.plan;
695   if (!plan) {
696     CoreTest.runner = CoreTest.Runner.create();
697     plan = CoreTest.plan = CoreTest.runner.plan;
698   }
699   return plan;
700 };
701 
702 // create a module.  If this is the first time, create the test plan and
703 // runner.  This will cause the test to run on page load
704 window.module = function(desc, l) {
705   CoreTest.defaultPlan().module(desc, l);
706 };
707 
708 // create a test.  If this is the first time, create the test plan and
709 // runner.  This will cause the test to run on page load
710 window.test = function(desc, func) {
711   CoreTest.defaultPlan().test(desc, func);
712 };
713 
714 // reset htmlbody for unit testing
715 window.clearHtmlbody = function() {
716   CoreTest.defaultPlan().clearHtmlbody();
717 };
718 
719 window.htmlbody = function(string) {
720   CoreTest.defaultPlan().htmlbody(string);
721 };
722