UNPKG

jaribu

Version:

a simple, full-featured, JavaScript testing framework

681 lines (618 loc) 20.6 kB
if (typeof define !== 'function') { var define = require('amdefine')(module); } define([ 'jaribu/colors', 'jaribu/display', 'jaribu/tools/Env', 'jaribu/Scaffolding', 'jaribu/Test', 'jaribu/Suite' ], function (c, display, Env, Scaffolding, Test, Suite) { "use strict"; var suites = []; var err_msg = ''; var pub = {}; var _ = {}; _.onComplete = function () {}; var runningInConsole = true; var runningInBrowser = false; if (typeof window === 'object') { runningInBrowser = true; runningInConsole = false; } function buildTestObj(env, s, t) { if (! t.desc ) { err_msg = s.name + ": test '" + t.name + "'' requires a 'desc' property"; return false; } else if (typeof t.run !== 'function') { err_msg = s.name + ": test '" + t.name + "'' requires a 'run' function"; return false; } var test = new Test(); //test.name = t.name; test.desc = t.desc; test.setup = new Scaffolding(); test.actual = new Scaffolding(); test.takedown = new Scaffolding(); // even though before/afterEach are defined // at the suite level, we need a separate object // for each test to run so that it's environment // is preserved and there's no bleed. test.beforeEach = new Scaffolding(); test.afterEach = new Scaffolding(); test.beforeEach.env = env; test.afterEach.env = env; if (typeof s.beforeEach === 'function') { test.beforeEach.run = s.beforeEach; } if (typeof s.afterEach === 'function') { test.afterEach.run = s.afterEach; } test.setup.env = env; test.actual.env = env; test.takedown.env = env; test.actual.run = t.run; test.actual.willFail = undefined; if (typeof t.setup === 'function') { test.setup.run = t.setup; } if (typeof t.takedown === 'function') { test.takedown.run = t.takedown; } // figureout if there is a timeout to override default if (typeof t.timeout === 'number') { test.actual.timeout = t.timeout; test.setup.timeout = t.timeout; test.takedown.timeout = t.timeout; } if (typeof t.willFail === 'boolean') { // if true, a failing test passes, and passing test fails test.actual.willFail = t.willFail; } //console.log(test); return test; } function buildSuiteObj(suite, env, s) { suite.name = s.name; suite.desc = s.desc; suite.setup = new Scaffolding(); suite.takedown = new Scaffolding(); suite.setup.env = env; suite.takedown.env = env; if (typeof s.setup === 'function') { suite.setup.run = s.setup; } if (typeof s.takedown === 'function') { suite.takedown.run = s.takedown; } if (typeof s.abortOnFail === 'boolean') { // if true, abort execution suite.abortOnFail = s.abortOnFail; } return suite; } /** * load a single suite json object into the library * * @param {object} s suite object from test file * @return {boolean} success of loading */ pub.loadSuite = function (s) { if (! s.desc ) { err_msg = '... suite requires a \'desc\' property'; return false; } else if (! s.tests ) { err_msg = '... suite requires a \'tests\' array'; return false; } else if ((runningInBrowser) && ((typeof s.runInBrowser === 'boolean') && (! s.runInBrowser))) { err_msg = '... suite should not run in the browser. skipping.'; return false; } else if ((runningInConsole) && ((typeof s.runInConsole === 'boolean') && (! s.runInConsole))) { err_msg = '... suite should not run in the browser. skipping.'; return false; } /* * Create all the test objects from the JSON data */ var tests = []; var suite = new Suite(); // we define this early so we can assign it // as parent to test objects var env = new Env(); if (typeof s.timeout === 'number') { // override test timeouts with Suite timeout Test.prototype.timeout = s.timeout; // override test timeouts with Suite timeout Scaffolding.prototype.timeout = s.timeout; } for (var i = 0, len = s.tests.length; i < len; i++) { var test = buildTestObj(env, s, s.tests[i]); // set position related attributes to test object test.position = i; if (i !== 0) { test.prev = tests[i - 1]; tests[i - 1].next = test; } test.parent = suite; tests.push(test); } /* * Create the suite object */ suite = buildSuiteObj(suite, env, s); suite.tests = tests; // set position related attributes to suite object suite.position = suites.length; if (suite.position !== 0) { suite.prev = suites[suite.position - 1]; suites[suite.position - 1].next = suite; } suites.push(suite); return true; }; /** * shall reset the jaribu enviroment to load new tests **/ pub.reset = function() { suites = []; }; /** * returns the error message * @return {string} error message */ pub.getErrorMessage = function () { return err_msg; }; /** * returns the number of suites loaded * @return {number} number of suites loaded */ pub.getNumSuites = function () { return suites.length; }; /** * returns the total number of tests in all the suites combined. * @return {number} total number of tests */ pub.getNumTests = function () { var total_tests = 0, i = 0; var num_suites = pub.getNumSuites(); for (i = 0; i < num_suites; i += 1) { total_tests = total_tests + suites[i].tests.length; } return total_tests; }; /** * begins the test cyle, by activating the first suite * @param {function} onComplete function to call when all tests are * complete * @return {} */ pub.begin = function (onComplete) { _.onComplete = onComplete; function errFunction (error) { if ((typeof(_.current) === 'object') && (! _.current._genertingStackTrace) && (! _.current.willThrow)) { _.current.result(false, "\n" + error, error.stack); } else if (((typeof(_.current) !== 'object') || (( !_.current.willThrow)) && ( !_.current.genertingStackTrace))) { console.error("Uncaught exception without test context: ", "\n", error, error.stack); process.exit(-1); } } if (typeof process !== 'undefined') { process.on('uncaughtException', errFunction); } else { window.addEventListener('error', errFunction); } var num_suites = pub.getNumSuites(); var total_tests = pub.getNumTests(); display.begin(num_suites, total_tests); if (suites[0]) { run('setup', 0); } }; /** * test object is passed here when it passes the test, setup or takedown. * handles printing the result, and updating the objects status. * * @param {string} part - indicates the portion of test just run * @param {number} suiteIndex - the index number of the test run * @param {number} testIndex - the index number of the test run [optional] */ function pass(part, suiteIndex, testIndex) { var o; var isSuite = false; if (typeof testIndex === 'number') { o = suites[suiteIndex].tests[testIndex]; } else { isSuite = true; o = suites[suiteIndex]; } display.pass(part, o); if ( isSuite ) { // Suite ---------------------------- if (part === 'setup') { // setup completed o.setup.status = true; // run the next test in suite if (!testIndex) { testIndex = 0; } else { testIndex = testIndex + 1; } if (typeof o.tests[testIndex] === 'object') { run('beforeEach', suiteIndex, testIndex); } else { run('takedown', suiteIndex); } } else if (part === 'takedown') { // takedown completed o.takedown.status = true; if (o.next) { // move on to the next suite run('setup', suiteIndex + 1); } else { // finished, show summary results showSummary(); } } } else { // Test ---------------------------------------------- if (part === 'beforeEach') { // beforeEach completed o.beforeEach.status = true; // run the test setup run('setup', suiteIndex, testIndex); } else if (part === 'setup') { // setup completed o.setup.status = true; // run the test run('actual', suiteIndex, testIndex); } else if (part === 'takedown') { // takedown completed o.takedown.status = true; // call afterEach run('afterEach', suiteIndex, testIndex); } else if (part === 'afterEach') { // afterEach completed o.afterEach.status = true; if (typeof o.parent.tests[testIndex + 1] === 'object') { o.parent.testIndex = testIndex + 1; run('beforeEach', suiteIndex, testIndex + 1); } else { // run suites takedown run('takedown', suiteIndex); } } else { // test is complete o.status = true; run('takedown', suiteIndex, testIndex); } } } /** * test object is passed here when it fails the test, setup or takedown. * handles printing the result, and updating the objects status. * * @param {string} part - indicates the portion of test just run * @param {number} suiteIndex - the index number of the test run * @param {number} testIndex - the index number of the test run [optional] * @param {string} msg - any special failure message [optional] */ function fail(part, suiteIndex, testIndex, msg) { if (typeof testIndex === 'string') { msg = testIndex; testIndex = undefined; } var o; var isSuite = false; if (typeof testIndex === 'number') { o = suites[suiteIndex].tests[testIndex]; if (typeof msg === 'string') { suites[suiteIndex].tests[testIndex][part].failmsg = msg; } display.details('test', o); } else { isSuite = true; o = suites[suiteIndex]; if (typeof msg === 'string') { suites[suiteIndex][part].failmsg = msg; } display.details('suite', o); } display.fail(part, o); // if we've failed, we always perform the takedown. if (part === 'takedown') { // takedown has been done o.takedown.status = false; if (( isSuite ) && (! o.abortOnFail)) { // run next suite run('setup', o.next); } else { // run afterEach for this test run('afterEach', suiteIndex, testIndex); } } else if (part === 'afterEach') { o.afterEach.status = false; if (typeof o.parent.tests[o.parent.testIndex] === 'object') { if (! o.parent.abortOnFail) { var ti = o.parent.testIndex; o.parent.testIndex = o.parent.testIndex + 1; run('beforeEach', suiteIndex, ti); } } else { // run suites takedown run('takedown', suiteIndex); } } else if (part === 'beforeEach') { o.beforeEach.status = false; run('afterEach', suiteIndex, testIndex); } else if (part === 'setup') { o.setup.status = false; run('takedown', suiteIndex, testIndex); } else if (part === 'actual') { // the actual test o.status = false; if (o.parent.abortOnFail) { display.print('test failed with abortOnFail set... aborting.'); showSummary(); } run('takedown', suiteIndex, testIndex); } else { throw new Error('no part specified in run()'); } } /** * generically handles each aspect of a suite/test setup/run/takedown * using the commonalities in each of the objects methods, and the * chaining references (o.next). * * @param {string} part - the portion of test to be run. * (setup, beforeEach, etc.) if undefined * assumes 'actual' test * @param {number} suiteIndex - the index number of the test to run * @param {number} testIndex - the index number of the test to run [optional] * */ function run(part, suiteIndex, testIndex) { var local; var o; var isSuite = true; if (typeof testIndex === 'number') { isSuite = false; o = suites[suiteIndex].tests[testIndex]; } else { o = suites[suiteIndex]; } if ( part === 'setup' ) { if ( isSuite ) { display.suiteBorder(); display.details('suite', o); // display.setup('suite'); } else { // display.linebreak(); //display.details('test', o); // display.setup('test'); } local = o.setup; } else if ( part === 'beforeEach' ) { //display.beforeEach(); local = o.beforeEach; } else if ( part === 'afterEach' ) { //display.afterEach(); local = o.afterEach; } else if ( part === 'takedown' ) { // if ( isSuite ) { // display.takedown('suite'); // } else { // display.takedown('test'); // } local = o.takedown; } else { // must be a test local = o.actual; } executeTest(part, local, suiteIndex, testIndex); } function executeTest(part, local, suiteIndex, testIndex) { // // we run the test in the next tick so that the function returns and // we don't build up the call stack // setTimeout(function () { // save reference to current test, so the 'uncaughtException' handler can fail // the right test. _.current = local; try { // // some objects should be given the current test to avoid requiring the // tester to pass in the object to apply the results against. // if (local.WebSocketClient.prototype) {// FIXME no proper test for existence local.WebSocketClient.prototype.setTest(local); } // set running flag local._running = true; var ret = local.run(local.env.get(), local); if (ret && typeof(ret.then) === 'function') { ret.then( function () { local.result(true, 'promise fulfilled'); }, function (err) { var stack; if (err.hasOwnProperty('stack')) { stack = err.stack; } else { stack = new Error('').stack; } local.result(false, 'promise failed ' + err, stack); } ); } // if the promise library has a fail function, we can catch unexpected errors if (ret && typeof(ret.fail) === 'function') { ret.fail(function (err) { var stack; if (err.hasOwnProperty('stack')) { stack = err.stack; } else { stack = new Error('').stack; } local.result(false, 'promise failed with an exception ' + err, stack); }); } } catch (err) { //console.log('LOCAL: ',local); if (err.hasOwnProperty('stack')) { local.result(false, "\n"+err, err.stack); } else { local.result(false, "\n"+err); } } waitResult(part, suiteIndex, testIndex, local); }, 0); } function waitResult(part, suiteIndex, testIndex, local) { // this is function calls itself after a set interval, checking the // status of the test via. the result property. // result is initialized as undefined, and the test is not complete // until it is 'true' or 'false'. var processed = false; var waitCount = 0; var waitInterval = 50; (function _waitResult() { if (processed) { console.log('test processed, aborting waitResult'); return; } else if (local.result() === undefined) { if (waitCount < local.timeout) { waitCount = waitCount + waitInterval; setTimeout(_waitResult, waitInterval); return; } else { if (local.willFail === true) { display.print('!'); pass(part, suiteIndex, testIndex); } else { fail(part, suiteIndex, testIndex, 'timeout'); } } } else if (local.result() === false) { if (local.willFail === true) { display.print('!'); pass(part, suiteIndex, testIndex); } else { fail(part, suiteIndex, testIndex); } } else if (local.result() === true) { if ((typeof local.willFail === 'boolean') && (local.willFail === true)) { display.print('!'); fail(part, suiteIndex, testIndex); } else { pass(part, suiteIndex, testIndex); } } else { display.print(c.red + 'ERROR GETTING RESULT' + c.reset); fail(part, suiteIndex, testIndex); } processed = true; }()); } function getTestSummary(summary, suite) { // iterate through tests for this suite, populating the summary for (var i = 0, len = suite.tests.length; i < len; i++) { var t = suite.tests[i]; var types = [ 'setup', 'takedown', 'beforeEach', 'afterEach' ]; summary.scaffolding.total = summary.scaffolding.total + 2; for (var j = 0, jlen = types.length; j < jlen; j++) { if (typeof t[types[j]].status === 'undefined') { summary.scaffolding.skipped = summary.scaffolding.skipped + 1; } else if (!t[types[j]].status) { summary.scaffolding.failed = summary.scaffolding.failed + 1; summary.scaffolding.failObjs.push({ name: 'test', type: types[j], obj: t }); } else { summary.scaffolding.passed = summary.scaffolding.passed + 1; } } summary.tests.total = summary.tests.total + 1; if (typeof t.status === 'undefined') { summary.tests.skipped = summary.tests.skipped + 1; } else if (!t.status) { summary.tests.failed = summary.tests.failed + 1; summary.tests.failObjs.push({ name: 'test', type: 'actual', obj: t }); } else { summary.tests.passed = summary.tests.passed + 1; } } } function getSuiteSummary(summary, suite) { // suite setup & takedown summarization var types = ['setup', 'takedown']; for (var i = 0, len = types.length; i < len; i++) { if (typeof suite[types[i]].status === 'undefined') { summary.scaffolding.skipped = summary.scaffolding.skipped + 1; } else if (!suite[types[i]].status) { summary.scaffolding.failed = summary.scaffolding.failed + 1; summary.scaffolding.failObjs.push({ name: 'suite', type: types[i], obj: suite }); } else { summary.scaffolding.passed = summary.scaffolding.passed + 1; } } } function getSummary() { var summary = { 'scaffolding': { 'total': 0, 'failed': 0, 'passed': 0, 'skipped': 0, 'failObjs': [] }, 'tests': { 'total': 0, 'failed': 0, 'passed': 0, 'skipped': 0, 'failObjs': [] } }; for (var i = 0, num_suites = pub.getNumSuites(); i < num_suites; i++) { summary.scaffolding.total = summary.scaffolding.total + 2; getSuiteSummary(summary, suites[i]); getTestSummary(summary, suites[i]); } return summary; } function showSummary() { var num_suites = pub.getNumSuites(); var summary = getSummary(); display.summary(num_suites, summary); // call specified onComplete function _.onComplete(); if (((summary.tests.failed > 0) || (summary.tests.failed > 0)) || ((summary.scaffolding.failed > 0) || (summary.scaffolding.failed > 0))) { display.printn(c.redbg + ' FAIL' + c.reset + c.red + ' some tests failed!' + c.reset); if (typeof process !== 'undefined') { process.exit(1); } } else { display.printn(c.greenbg + ' OK ' + c.reset + c.green + ' all tests passed!' + c.reset); if (typeof process !== 'undefined') { process.exit(0); } } } pub.display = display; return pub; });