blob: 8ff031b81d11e7a83d60c2da0de6a1106298b42f [file] [log] [blame]
/*!
* 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;
};