/*! * Nodeunit * Copyright (c) 2010 Caolan McMahon * MIT Licensed * * THIS FILE SHOULD BE BROWSER-COMPATIBLE JS! * You can use @REMOVE_LINE_FOR_BROWSER to remove code from the browser build. * Only code on that line will be removed, it's mostly to avoid requiring code * that is node specific */ /** * Module dependencies */ var async = require('../deps/async'), //@REMOVE_LINE_FOR_BROWSER nodeunit = require('./nodeunit'), //@REMOVE_LINE_FOR_BROWSER types = require('./types'); //@REMOVE_LINE_FOR_BROWSER /** * Added for browser compatibility */ var _keys = function (obj) { if (Object.keys) { return Object.keys(obj); } var keys = []; for (var k in obj) { if (obj.hasOwnProperty(k)) { keys.push(k); } } return keys; }; var _copy = function (obj) { var nobj = {}; var keys = _keys(obj); for (var i = 0; i < keys.length; i += 1) { nobj[keys[i]] = obj[keys[i]]; } return nobj; }; /** * Runs a test function (fn) from a loaded module. After the test function * calls test.done(), the callback is executed with an assertionList as its * second argument. * * @param {String} name * @param {Function} fn * @param {Object} opt * @param {Function} callback * @api public */ exports.runTest = function (name, fn, opt, callback) { var options = types.options(opt); options.testStart(name); var start = new Date().getTime(); var test = types.test(name, start, options, callback); options.testReady(test); try { fn(test); } catch (e) { test.done(e); } }; /** * Takes an object containing test functions or other test suites as properties * and runs each in series. After all tests have completed, the callback is * called with a list of all assertions as the second argument. * * If a name is passed to this function it is prepended to all test and suite * names that run within it. * * @param {String} name * @param {Object} suite * @param {Object} opt * @param {Function} callback * @api public */ exports.runSuite = function (name, suite, opt, callback) { suite = wrapGroup(suite); var keys = _keys(suite); async.concatSeries(keys, function (k, cb) { var prop = suite[k], _name; _name = name ? [].concat(name, k) : [k]; _name.toString = function () { // fallback for old one return this.join(' - '); }; if (typeof prop === 'function') { var in_name = false, in_specific_test = (_name.toString() === opt.testFullSpec) ? true : false; for (var i = 0; i < _name.length; i += 1) { if (_name[i] === opt.testspec) { in_name = true; } } if ((!opt.testFullSpec || in_specific_test) && (!opt.testspec || in_name)) { if (opt.moduleStart) { opt.moduleStart(); } exports.runTest(_name, suite[k], opt, cb); } else { return cb(); } } else { exports.runSuite(_name, suite[k], opt, cb); } }, callback); }; /** * Run each exported test function or test suite from a loaded module. * * @param {String} name * @param {Object} mod * @param {Object} opt * @param {Function} callback * @api public */ exports.runModule = function (name, mod, opt, callback) { var options = _copy(types.options(opt)); var _run = false; var _moduleStart = options.moduleStart; mod = wrapGroup(mod); function run_once() { if (!_run) { _run = true; _moduleStart(name); } } options.moduleStart = run_once; var start = new Date().getTime(); exports.runSuite(null, mod, options, function (err, a_list) { var end = new Date().getTime(); var assertion_list = types.assertionList(a_list, end - start); options.moduleDone(name, assertion_list); if (nodeunit.complete) { nodeunit.complete(name, assertion_list); } callback(null, a_list); }); }; /** * Treats an object literal as a list of modules keyed by name. Runs each * module and finished with calling 'done'. You can think of this as a browser * safe alternative to runFiles in the nodeunit module. * * @param {Object} modules * @param {Object} opt * @api public */ // TODO: add proper unit tests for this function exports.runModules = function (modules, opt) { var all_assertions = []; var options = types.options(opt); var start = new Date().getTime(); async.concatSeries(_keys(modules), function (k, cb) { exports.runModule(k, modules[k], options, cb); }, function (err, all_assertions) { var end = new Date().getTime(); options.done(types.assertionList(all_assertions, end - start)); }); }; /** * Wraps a test function with setUp and tearDown functions. * Used by testCase. * * @param {Function} setUp * @param {Function} tearDown * @param {Function} fn * @api private */ var wrapTest = function (setUp, tearDown, fn) { return function (test) { var context = {}; if (tearDown) { var done = test.done; test.done = function (err) { try { tearDown.call(context, function (err2) { if (err && err2) { test._assertion_list.push( types.assertion({error: err}) ); return done(err2); } done(err || err2); }); } catch (e) { done(e); } }; } if (setUp) { setUp.call(context, function (err) { if (err) { return test.done(err); } fn.call(context, test); }); } else { fn.call(context, test); } }; }; /** * Returns a serial callback from two functions. * * @param {Function} funcFirst * @param {Function} funcSecond * @api private */ var getSerialCallback = function (fns) { if (!fns.length) { return null; } return function (callback) { var that = this; var bound_fns = []; for (var i = 0, len = fns.length; i < len; i++) { (function (j) { bound_fns.push(function () { return fns[j].apply(that, arguments); }); })(i); } return async.series(bound_fns, callback); }; }; /** * Wraps a group of tests with setUp and tearDown functions. * Used by testCase. * * @param {Object} group * @param {Array} setUps - parent setUp functions * @param {Array} tearDowns - parent tearDown functions * @api private */ var wrapGroup = function (group, setUps, tearDowns) { var tests = {}; var setUps = setUps ? setUps.slice(): []; var tearDowns = tearDowns ? tearDowns.slice(): []; if (group.setUp) { setUps.push(group.setUp); delete group.setUp; } if (group.tearDown) { tearDowns.unshift(group.tearDown); delete group.tearDown; } var keys = _keys(group); for (var i = 0; i < keys.length; i += 1) { var k = keys[i]; if (typeof group[k] === 'function') { tests[k] = wrapTest( getSerialCallback(setUps), getSerialCallback(tearDowns), group[k] ); } else if (typeof group[k] === 'object') { tests[k] = wrapGroup(group[k], setUps, tearDowns); } } return tests; }; /** * Backwards compatibility for test suites using old testCase API */ exports.testCase = function (suite) { return suite; };