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