// ========================================================================== // Project: SproutCore
Costello - Property Observing Library // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================
/*globals CoreTest Q$ */
var QUNIT_BREAK_ON_TEST_FAIL = false;
/** @class
A test plan contains a set of functions that will be executed in order. The results will be recorded into a results hash as well as calling a delegate. When you define tests and modules, you are adding to the active test plan. The test plan is then run when the page has finished loading. Normally you will not need to work with a test plan directly, though if you are writing a test runner application that needs to monitor test progress you may write a delegate to talk to the test plan. The CoreTest.Plan.fn hash contains functions that will be made global via wrapper methods. The methods must accept a Plan object as their first parameter. ## Results The results hash contains a summary of the results of running the test plan. It includes the following properties: - *assertions* -- the total number of assertions - *tests* -- the total number of tests - *passed* -- number of assertions that passed - *failed* -- number of assertions that failed - *errors* -- number of assertions with errors - *warnings* -- number of assertions with warnings You can also consult the log property, which contains an array of hashes - one for each assertion - with the following properties: - *module* -- module descriptions - *test* -- test description - *message* -- assertion description - *result* -- CoreTest.OK, CoreTest.FAILED, CoreTest.ERROR, CoreTest.WARN @since SproutCore 1.0
*/ CoreTest.Plan = {
/** Define a new test plan instance. Optionally pass attributes to apply to the new plan object. Usually you will call this without arguments. @param {Hash} attrs plan arguments @returns {CoreTest.Plan} new instance/subclass */ create: function(attrs) { var len = arguments.length, ret = CoreTest.beget(this), idx; for(idx=0;idx<len;idx++) CoreTest.mixin(ret, attrs); ret.queue = ret.queue.slice(); // want an independent queue return ret ; }, // .......................................................... // RUNNING // /** @private - array of functions to execute in order. */ queue: [], /** If true then the test plan is currently running and items in the queue will execute in order. @type {Boolean} */ isRunning: false, /** Primitive used to add callbacks to the test plan queue. Usually you will not want to call this method directly but instead use the module() or test() methods. @returns {CoreTest.Plan} receiver */ synchronize: function synchronize(callback) { this.queue.push(callback); if (this.isRunning) this.process(); // run queue return this; }, /** Processes items in the queue as long as isRunning remained true. When no further items are left in the queue, calls finish(). Usually you will not call this method directly. Instead call run(). @returns {CoreTest.Plan} receiver */ process: function process() { while(this.queue.length && this.isRunning) { this.queue.shift().call(this); } return this ; }, /** Begins running the test plan after a slight delay to avoid interrupting any current callbacks. @returns {CoreTest.Plan} receiver */ start: function() { var plan = this ; setTimeout(function() { if (plan.timeout) clearTimeout(plan.timeout); plan.timeout = null; plan.isRunning = true; plan.process(); }, 13); return this ; }, /** Stops the test plan from running any further. If you pass a timeout, it will raise an exception if the test plan does not begin executing with the allotted timeout. @param {Number} timeout optional timeout in msec @returns {CoreTest.Plan} receiver */ stop: function(timeout) { this.isRunning = false ; if (this.timeout) clearTimeout(this.timeout); if (timeout) { var plan = this; this.timeout = setTimeout(function() { plan.fail("Test timed out").start(); }, timeout); } else this.timeout = null ; return this ; }, /** Force the test plan to take a break. Avoids slow script warnings. This is called automatically after each test completes. */ pause: function() { if (this.isRunning) { var del = this.delegate; if (del && del.planDidPause) del.planDidPause(this); this.isRunning = false ; this.start(); } return this ; }, /** Initiates running the tests for the first time. This will add an item to the queue to call finish() on the plan when the run completes. @returns {CoreTest.Plan} receiver */ run: function() { this.isRunning = true; this.prepare(); // initialize new results this.results = { start: new Date().getTime(), finish: null, runtime: 0, tests: 0, total: 0, passed: 0, failed: 0, errors: 0, warnings: 0, assertions: [] }; // add item to queue to finish running the test plan when finished. this.begin().synchronize(this.finish).process(); return this ; }, /** Called when the test plan begins running. This method will notify the delegate. You will not normally call this method directly. @returns {CoreTest.Plan} receiver */ begin: function() { var del = this.delegate; if (del && del.planDidBegin) del.planDidBegin(this); return this ; }, /** When the test plan finishes running, this method will be called to notify the delegate that the plan as finished. @returns {CoreTest.Plan} receiver */ finish: function() { var r = this.results, del = this.delegate; r.finish = new Date().getTime(); r.runtime = r.finish - r.start; if (del && del.planDidFinish) del.planDidFinish(this, r); return this ; }, /** Sets the current module information. This will be used when a test is added under the module. @returns {CoreTest.Plan} receiver */ module: function(desc, lifecycle) { if (typeof SC !== 'undefined' && SC.filename) { desc = SC.filename.replace(/^.+?\/current\/tests\//,'') + '\n' + desc; } this.currentModule = desc; if (!lifecycle) lifecycle = {}; this.setup(lifecycle.setup).teardown(lifecycle.teardown); return this ; }, /** Sets the current setup method. @returns {CoreTest.Plan} receiver */ setup: function(func) { this.currentSetup = func || CoreTest.K; return this; }, /** Sets the current teardown method @returns {CoreTest.Plan} receiver */ teardown: function teardown(func) { this.currentTeardown = func || CoreTest.K ; return this; }, now: function() { return new Date().getTime(); }, /** Generates a unit test, adding it to the test plan. */ test: function test(desc, func) { if (!this.enabled(this.currentModule, desc)) return this; // skip // base prototype describing test var working = { module: this.currentModule, test: desc, expected: 0, assertions: [] }; var msg; var name = desc ; if (this.currentModule) name = this.currentModule + " module: " + name; var setup = this.currentSetup || CoreTest.K; var teardown = this.currentTeardown || CoreTest.K; // add setup to queue this.synchronize(function() { // save main fixture... var mainEl = document.getElementById('main'); this.fixture = mainEl ? mainEl.innerHTML : ''; mainEl = null; this.working = working; try { working.total_begin = working.setup_begin = this.now(); setup.call(this); working.setup_end = this.now(); } catch(e) { msg = (e && e.toString) ? e.toString() : "(unknown error)"; this.error("Setup exception on " + name + ": " + msg); } }); // now actually invoke test this.synchronize(function() { if (!func) { this.warn("Test not yet implemented: " + name); } else { try { if (CoreTest.trace) console.log("run: " + name); this.working.test_begin = this.now(); func.call(this); this.working.test_end = this.now(); } catch(e) { msg = (e && e.toString) ? e.toString() : "(unknown error)"; this.error("Died on test #" + (this.working.assertions.length + 1) + ": " + msg); } } }); // cleanup this.synchronize(function() { try { this.working.teardown_begin = this.now(); teardown.call(this); this.working.teardown_end = this.now(); } catch(e) { msg = (e && e.toString) ? e.toString() : "(unknown error)"; this.error("Teardown exception on " + name + ": " + msg); } }); // finally, reset and report result this.synchronize(function() { if (this.reset) { try { this.working.reset_begin = this.now(); this.reset(); this.working.total_end = this.working.reset_end = this.now(); } catch(ex) { msg = (ex && ex.toString) ? ex.toString() : "(unknown error)"; this.error("Reset exception on " + name + ": " + msg) ; } } // check for expected assertions var w = this.working, exp = w.expected, len = w.assertions.length; if (exp && exp !== len) { this.fail("Expected " + exp + " assertions, but " + len + " were run"); } // finally, record result this.working = null; this.record(w.module, w.test, w.assertions, w); if (!this.pauseTime) { this.pauseTime = new Date().getTime(); } else { var now = new Date().getTime(); if ((now - this.pauseTime) > 250) { this.pause(); this.pauseTime = now ; } } }); }, clearHtmlbody: function(){ var body = Q$('body')[0]; // first, find the first element with id 'htmlbody-begin' if exists, // remove everything after that to reset... var begin = Q$('body #htmlbody-begin')[0]; if (!begin) { begin = Q$('<div id="htmlbody-begin"></div>')[0]; body.appendChild(begin); } else { while(begin.nextSibling) body.removeChild(begin.nextSibling); } begin = null; }, /** Converts the passed string into HTML and then appends it to the main body element. This is a useful way to automatically load fixture HTML into the main page. */ htmlbody: function htmlbody(string) { var html = Q$(string) ; var body = Q$('body')[0]; this.clearHtmlbody(); // now append new content html.each(function() { body.appendChild(this); }); }, /** Records the results of a test. This will add the results to the log and notify the delegate. The passed assertions array should contain hashes with the result and message. */ record: function(module, test, assertions, timings) { var r = this.results, len = assertions.length, del = this.delegate, idx, cur; r.tests++; for(idx=0;idx<len;idx++) { cur = assertions[idx]; cur.module = module; cur.test = test ; r.total++; r[cur.result]++; r.assertions.push(cur); } if (del && del.planDidRecord) { del.planDidRecord(this, module, test, assertions, timings) ; } }, /** Universal method can be called to reset the global state of the application for each test. The default implementation will reset any saved fixture. */ reset: function() { if (this.fixture) { var mainEl = document.getElementById('main'); if (mainEl) mainEl.innerHTML = this.fixture; mainEl = null; } return this ; }, /** Can be used to decide if a particular test should be enabled or not. Current implementation allows a test to run. @returns {Boolean} */ enabled: function(moduleName, testName) { return true; }, // .......................................................... // MATCHERS // /** Called by a matcher to record that a test has passed. Requires a working test property. */ pass: function(msg) { var w = this.working ; if (!w) throw new Error("pass("+msg+") called outside of a working test"); w.assertions.push({ message: msg, result: CoreTest.OK }); return this ; }, /** Called by a matcher to record that a test has failed. Requires a working test property. */ fail: function(msg) { var w = this.working ; if (!w) throw new Error("fail("+msg+") called outside of a working test"); w.assertions.push({ message: msg, result: CoreTest.FAIL }); return this ; }, /** Called by a matcher to record that a test issued a warning. Requires a working test property. */ warn: function(msg) { var w = this.working ; if (!w) throw new Error("warn("+msg+") called outside of a working test"); w.assertions.push({ message: msg, result: CoreTest.WARN }); return this ; }, /** Called by a matcher to record that a test had an error. Requires a working test property. */ error: function(msg, e) { var w = this.working ; if (!w) throw new Error("error("+msg+") called outside of a working test"); if(e && typeof console != "undefined" && console.error && console.warn ) { console.error(msg); console.error(e); } w.assertions.push({ message: msg, result: CoreTest.ERROR }); return this ; }, /** Any methods added to this hash will be made global just before the first test is run. You can add new methods to this hash to use them in unit tests. "this" will always be the test plan. */ fn: { /** Primitive will pass or fail the test based on the first boolean. If you pass an actual and expected value, then this will automatically log the actual and expected values. Otherwise, it will expect the message to be passed as the second argument. @param {Boolean} pass true if pass @param {Object} actual optional actual @param {Object} expected optional expected @param {String} msg optional message @returns {CoreTest.Plan} receiver */ ok: function ok(pass, actual, expected, msg) { if (msg === undefined) { msg = actual ; if (!msg) msg = pass ? "OK" : "failed"; } else { if (!msg) msg = pass ? "OK" : "failed"; if (pass) { msg = msg + ": " + CoreTest.dump(expected) ; } else { msg = msg + ", expected: " + CoreTest.dump(expected) + " result: " + CoreTest.dump(actual); } } if (QUNIT_BREAK_ON_TEST_FAIL & !pass) { SC.throw(msg); } return !!pass ? this.pass(msg) : this.fail(msg); }, /** Primitive performs a basic equality test on the passed values. Prints out both actual and expected values. Preferred to ok(actual === expected, message); @param {Object} actual tested object @param {Object} expected expected value @param {String} msg optional message @returns {CoreTest.Plan} receiver */ equals: function equals(actual, expected, msg) { if (msg === undefined) msg = null; // make sure ok logs properly return this.ok(actual == expected, actual, expected, msg); }, /** Expects the passed function call to throw an exception of the given type. If you pass null or Error for the expected exception, this will pass if any error is received. If you pass a string, this will check message property of the exception. @param {Function} callback the function to execute @param {Error} expected optional, the expected error @param {String} a description @returns {CoreTest.Plan} receiver */ should_throw: function should_throw(callback, expected, msg) { var actual = false ; try { callback(); } catch(e) { actual = (typeof expected === "string") ? e.message : e; } if (expected===false) { ok(actual===false, CoreTest.fmt("%@ expected no exception, actual %@", msg, actual)); } else if (expected===Error || expected===null || expected===true) { ok(!!actual, CoreTest.fmt("%@ expected exception, actual %@", msg, actual)); } else { equals(actual, expected, msg); } }, /** Specify the number of expected assertions to gaurantee that a failed test (no assertions are run at all) don't slip through @returns {CoreTest.Plan} receiver */ expect: function expect(asserts) { this.working.expected = asserts; }, /** Verifies that two objects are actually the same. This method will do a deep compare instead of a simple equivalence test. You should use this instead of equals() when you expect the two object to be different instances but to have the same content. @param {Object} value tested object @param {Object} actual expected value @param {String} msg optional message @returns {CoreTest.Plan} receiver */ same: function(actual, expected, msg) { if (msg === undefined) msg = null ; // make sure ok logs properly return this.ok(CoreTest.equiv(actual, expected), actual, expected, msg); }, /** Logs a warning. Useful for clearly marking assertions that will need to be revisited after future development. Warnings are highlighted prominently in the test runner, but do not mark the test suite as failed. @param {String} msg the warning message @returns {CoreTest.Plan} receiver */ warn: function(msg) { if (msg === undefined) msg = null; return this.warn(msg); }, /** Stops the current tests from running. An optional timeout will automatically fail the test if it does not restart within the specified period of time. @param {Number} timeout timeout in msec @returns {CoreTest.Plan} receiver */ stop: function(timeout) { return this.stop(timeout); }, /** Restarts tests running. Use this to begin tests after you stop tests. @returns {CoreTest.Plan} receiver */ start: function() { return this.start(); }, reset: function() { return this.reset(); } }, /** Exports the comparison functions into the global namespace. This will allow you to call these methods from within testing functions. This method is called automatically just before the first test is run. @returns {CoreTest.Plan} receiver */ prepare: function() { var fn = this.fn, plan = this, key, func; for(key in fn) { if (!fn.hasOwnProperty(key)) continue ; func = fn[key]; if (typeof func !== "function") continue ; window[key] = this._bind(func); if (!plan[key]) plan[key] = func; } return this ; }, _bind: function(func) { var plan = this; return function() { return func.apply(plan, arguments); }; }
};
// .….….….….….….….….….….….….….….. // EXPORT BASIC API //
CoreTest.defaultPlan = function defaultPlan() {
var plan = CoreTest.plan; if (!plan) { CoreTest.runner = CoreTest.Runner.create(); plan = CoreTest.plan = CoreTest.runner.plan; } return plan;
};
// create a module. If this is the first time, create the test plan and // runner. This will cause the test to run on page load window.module = function(desc, l) {
CoreTest.defaultPlan().module(desc, l);
};
// create a test. If this is the first time, create the test plan and // runner. This will cause the test to run on page load window.test = function(desc, func) {
CoreTest.defaultPlan().test(desc, func);
};
// reset htmlbody for unit testing window.clearHtmlbody = function() {
CoreTest.defaultPlan().clearHtmlbody();
};
window.htmlbody = function(string) {
CoreTest.defaultPlan().htmlbody(string);
};