696 lines
16 KiB
JavaScript
696 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Module dependencies.
|
|
* @private
|
|
*/
|
|
const {EventEmitter} = require('events');
|
|
const Hook = require('./hook');
|
|
var {
|
|
assignNewMochaID,
|
|
clamp,
|
|
constants: utilsConstants,
|
|
createMap,
|
|
defineConstants,
|
|
getMochaID,
|
|
inherits,
|
|
isString
|
|
} = require('./utils');
|
|
const debug = require('debug')('mocha:suite');
|
|
const milliseconds = require('ms');
|
|
const errors = require('./errors');
|
|
|
|
const {MOCHA_ID_PROP_NAME} = utilsConstants;
|
|
|
|
/**
|
|
* Expose `Suite`.
|
|
*/
|
|
|
|
exports = module.exports = Suite;
|
|
|
|
/**
|
|
* Create a new `Suite` with the given `title` and parent `Suite`.
|
|
*
|
|
* @public
|
|
* @param {Suite} parent - Parent suite (required!)
|
|
* @param {string} title - Title
|
|
* @return {Suite}
|
|
*/
|
|
Suite.create = function(parent, title) {
|
|
var suite = new Suite(title, parent.ctx);
|
|
suite.parent = parent;
|
|
title = suite.fullTitle();
|
|
parent.addSuite(suite);
|
|
return suite;
|
|
};
|
|
|
|
/**
|
|
* Constructs a new `Suite` instance with the given `title`, `ctx`, and `isRoot`.
|
|
*
|
|
* @public
|
|
* @class
|
|
* @extends EventEmitter
|
|
* @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter}
|
|
* @param {string} title - Suite title.
|
|
* @param {Context} parentContext - Parent context instance.
|
|
* @param {boolean} [isRoot=false] - Whether this is the root suite.
|
|
*/
|
|
function Suite(title, parentContext, isRoot) {
|
|
if (!isString(title)) {
|
|
throw errors.createInvalidArgumentTypeError(
|
|
'Suite argument "title" must be a string. Received type "' +
|
|
typeof title +
|
|
'"',
|
|
'title',
|
|
'string'
|
|
);
|
|
}
|
|
this.title = title;
|
|
function Context() {}
|
|
Context.prototype = parentContext;
|
|
this.ctx = new Context();
|
|
this.suites = [];
|
|
this.tests = [];
|
|
this.root = isRoot === true;
|
|
this.pending = false;
|
|
this._retries = -1;
|
|
this._beforeEach = [];
|
|
this._beforeAll = [];
|
|
this._afterEach = [];
|
|
this._afterAll = [];
|
|
this._timeout = 2000;
|
|
this._slow = 75;
|
|
this._bail = false;
|
|
this._onlyTests = [];
|
|
this._onlySuites = [];
|
|
assignNewMochaID(this);
|
|
|
|
Object.defineProperty(this, 'id', {
|
|
get() {
|
|
return getMochaID(this);
|
|
}
|
|
});
|
|
|
|
this.reset();
|
|
|
|
this.on('newListener', function(event) {
|
|
if (deprecatedEvents[event]) {
|
|
errors.deprecate(
|
|
'Event "' +
|
|
event +
|
|
'" is deprecated. Please let the Mocha team know about your use case: https://git.io/v6Lwm'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Inherit from `EventEmitter.prototype`.
|
|
*/
|
|
inherits(Suite, EventEmitter);
|
|
|
|
/**
|
|
* Resets the state initially or for a next run.
|
|
*/
|
|
Suite.prototype.reset = function() {
|
|
this.delayed = false;
|
|
function doReset(thingToReset) {
|
|
thingToReset.reset();
|
|
}
|
|
this.suites.forEach(doReset);
|
|
this.tests.forEach(doReset);
|
|
this._beforeEach.forEach(doReset);
|
|
this._afterEach.forEach(doReset);
|
|
this._beforeAll.forEach(doReset);
|
|
this._afterAll.forEach(doReset);
|
|
};
|
|
|
|
/**
|
|
* Return a clone of this `Suite`.
|
|
*
|
|
* @private
|
|
* @return {Suite}
|
|
*/
|
|
Suite.prototype.clone = function() {
|
|
var suite = new Suite(this.title);
|
|
debug('clone');
|
|
suite.ctx = this.ctx;
|
|
suite.root = this.root;
|
|
suite.timeout(this.timeout());
|
|
suite.retries(this.retries());
|
|
suite.slow(this.slow());
|
|
suite.bail(this.bail());
|
|
return suite;
|
|
};
|
|
|
|
/**
|
|
* Set or get timeout `ms` or short-hand such as "2s".
|
|
*
|
|
* @private
|
|
* @todo Do not attempt to set value if `ms` is undefined
|
|
* @param {number|string} ms
|
|
* @return {Suite|number} for chaining
|
|
*/
|
|
Suite.prototype.timeout = function(ms) {
|
|
if (!arguments.length) {
|
|
return this._timeout;
|
|
}
|
|
if (typeof ms === 'string') {
|
|
ms = milliseconds(ms);
|
|
}
|
|
|
|
// Clamp to range
|
|
var INT_MAX = Math.pow(2, 31) - 1;
|
|
var range = [0, INT_MAX];
|
|
ms = clamp(ms, range);
|
|
|
|
debug('timeout %d', ms);
|
|
this._timeout = parseInt(ms, 10);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set or get number of times to retry a failed test.
|
|
*
|
|
* @private
|
|
* @param {number|string} n
|
|
* @return {Suite|number} for chaining
|
|
*/
|
|
Suite.prototype.retries = function(n) {
|
|
if (!arguments.length) {
|
|
return this._retries;
|
|
}
|
|
debug('retries %d', n);
|
|
this._retries = parseInt(n, 10) || 0;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set or get slow `ms` or short-hand such as "2s".
|
|
*
|
|
* @private
|
|
* @param {number|string} ms
|
|
* @return {Suite|number} for chaining
|
|
*/
|
|
Suite.prototype.slow = function(ms) {
|
|
if (!arguments.length) {
|
|
return this._slow;
|
|
}
|
|
if (typeof ms === 'string') {
|
|
ms = milliseconds(ms);
|
|
}
|
|
debug('slow %d', ms);
|
|
this._slow = ms;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set or get whether to bail after first error.
|
|
*
|
|
* @private
|
|
* @param {boolean} bail
|
|
* @return {Suite|number} for chaining
|
|
*/
|
|
Suite.prototype.bail = function(bail) {
|
|
if (!arguments.length) {
|
|
return this._bail;
|
|
}
|
|
debug('bail %s', bail);
|
|
this._bail = bail;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Check if this suite or its parent suite is marked as pending.
|
|
*
|
|
* @private
|
|
*/
|
|
Suite.prototype.isPending = function() {
|
|
return this.pending || (this.parent && this.parent.isPending());
|
|
};
|
|
|
|
/**
|
|
* Generic hook-creator.
|
|
* @private
|
|
* @param {string} title - Title of hook
|
|
* @param {Function} fn - Hook callback
|
|
* @returns {Hook} A new hook
|
|
*/
|
|
Suite.prototype._createHook = function(title, fn) {
|
|
var hook = new Hook(title, fn);
|
|
hook.parent = this;
|
|
hook.timeout(this.timeout());
|
|
hook.retries(this.retries());
|
|
hook.slow(this.slow());
|
|
hook.ctx = this.ctx;
|
|
hook.file = this.file;
|
|
return hook;
|
|
};
|
|
|
|
/**
|
|
* Run `fn(test[, done])` before running tests.
|
|
*
|
|
* @private
|
|
* @param {string} title
|
|
* @param {Function} fn
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.beforeAll = function(title, fn) {
|
|
if (this.isPending()) {
|
|
return this;
|
|
}
|
|
if (typeof title === 'function') {
|
|
fn = title;
|
|
title = fn.name;
|
|
}
|
|
title = '"before all" hook' + (title ? ': ' + title : '');
|
|
|
|
var hook = this._createHook(title, fn);
|
|
this._beforeAll.push(hook);
|
|
this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_ALL, hook);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Run `fn(test[, done])` after running tests.
|
|
*
|
|
* @private
|
|
* @param {string} title
|
|
* @param {Function} fn
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.afterAll = function(title, fn) {
|
|
if (this.isPending()) {
|
|
return this;
|
|
}
|
|
if (typeof title === 'function') {
|
|
fn = title;
|
|
title = fn.name;
|
|
}
|
|
title = '"after all" hook' + (title ? ': ' + title : '');
|
|
|
|
var hook = this._createHook(title, fn);
|
|
this._afterAll.push(hook);
|
|
this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_ALL, hook);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Run `fn(test[, done])` before each test case.
|
|
*
|
|
* @private
|
|
* @param {string} title
|
|
* @param {Function} fn
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.beforeEach = function(title, fn) {
|
|
if (this.isPending()) {
|
|
return this;
|
|
}
|
|
if (typeof title === 'function') {
|
|
fn = title;
|
|
title = fn.name;
|
|
}
|
|
title = '"before each" hook' + (title ? ': ' + title : '');
|
|
|
|
var hook = this._createHook(title, fn);
|
|
this._beforeEach.push(hook);
|
|
this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_EACH, hook);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Run `fn(test[, done])` after each test case.
|
|
*
|
|
* @private
|
|
* @param {string} title
|
|
* @param {Function} fn
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.afterEach = function(title, fn) {
|
|
if (this.isPending()) {
|
|
return this;
|
|
}
|
|
if (typeof title === 'function') {
|
|
fn = title;
|
|
title = fn.name;
|
|
}
|
|
title = '"after each" hook' + (title ? ': ' + title : '');
|
|
|
|
var hook = this._createHook(title, fn);
|
|
this._afterEach.push(hook);
|
|
this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_EACH, hook);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Add a test `suite`.
|
|
*
|
|
* @private
|
|
* @param {Suite} suite
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.addSuite = function(suite) {
|
|
suite.parent = this;
|
|
suite.root = false;
|
|
suite.timeout(this.timeout());
|
|
suite.retries(this.retries());
|
|
suite.slow(this.slow());
|
|
suite.bail(this.bail());
|
|
this.suites.push(suite);
|
|
this.emit(constants.EVENT_SUITE_ADD_SUITE, suite);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Add a `test` to this suite.
|
|
*
|
|
* @private
|
|
* @param {Test} test
|
|
* @return {Suite} for chaining
|
|
*/
|
|
Suite.prototype.addTest = function(test) {
|
|
test.parent = this;
|
|
test.timeout(this.timeout());
|
|
test.retries(this.retries());
|
|
test.slow(this.slow());
|
|
test.ctx = this.ctx;
|
|
this.tests.push(test);
|
|
this.emit(constants.EVENT_SUITE_ADD_TEST, test);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Return the full title generated by recursively concatenating the parent's
|
|
* full title.
|
|
*
|
|
* @memberof Suite
|
|
* @public
|
|
* @return {string}
|
|
*/
|
|
Suite.prototype.fullTitle = function() {
|
|
return this.titlePath().join(' ');
|
|
};
|
|
|
|
/**
|
|
* Return the title path generated by recursively concatenating the parent's
|
|
* title path.
|
|
*
|
|
* @memberof Suite
|
|
* @public
|
|
* @return {string}
|
|
*/
|
|
Suite.prototype.titlePath = function() {
|
|
var result = [];
|
|
if (this.parent) {
|
|
result = result.concat(this.parent.titlePath());
|
|
}
|
|
if (!this.root) {
|
|
result.push(this.title);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Return the total number of tests.
|
|
*
|
|
* @memberof Suite
|
|
* @public
|
|
* @return {number}
|
|
*/
|
|
Suite.prototype.total = function() {
|
|
return (
|
|
this.suites.reduce(function(sum, suite) {
|
|
return sum + suite.total();
|
|
}, 0) + this.tests.length
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Iterates through each suite recursively to find all tests. Applies a
|
|
* function in the format `fn(test)`.
|
|
*
|
|
* @private
|
|
* @param {Function} fn
|
|
* @return {Suite}
|
|
*/
|
|
Suite.prototype.eachTest = function(fn) {
|
|
this.tests.forEach(fn);
|
|
this.suites.forEach(function(suite) {
|
|
suite.eachTest(fn);
|
|
});
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* This will run the root suite if we happen to be running in delayed mode.
|
|
* @private
|
|
*/
|
|
Suite.prototype.run = function run() {
|
|
if (this.root) {
|
|
this.emit(constants.EVENT_ROOT_SUITE_RUN);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determines whether a suite has an `only` test or suite as a descendant.
|
|
*
|
|
* @private
|
|
* @returns {Boolean}
|
|
*/
|
|
Suite.prototype.hasOnly = function hasOnly() {
|
|
return (
|
|
this._onlyTests.length > 0 ||
|
|
this._onlySuites.length > 0 ||
|
|
this.suites.some(function(suite) {
|
|
return suite.hasOnly();
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Filter suites based on `isOnly` logic.
|
|
*
|
|
* @private
|
|
* @returns {Boolean}
|
|
*/
|
|
Suite.prototype.filterOnly = function filterOnly() {
|
|
if (this._onlyTests.length) {
|
|
// If the suite contains `only` tests, run those and ignore any nested suites.
|
|
this.tests = this._onlyTests;
|
|
this.suites = [];
|
|
} else {
|
|
// Otherwise, do not run any of the tests in this suite.
|
|
this.tests = [];
|
|
this._onlySuites.forEach(function(onlySuite) {
|
|
// If there are other `only` tests/suites nested in the current `only` suite, then filter that `only` suite.
|
|
// Otherwise, all of the tests on this `only` suite should be run, so don't filter it.
|
|
if (onlySuite.hasOnly()) {
|
|
onlySuite.filterOnly();
|
|
}
|
|
});
|
|
// Run the `only` suites, as well as any other suites that have `only` tests/suites as descendants.
|
|
var onlySuites = this._onlySuites;
|
|
this.suites = this.suites.filter(function(childSuite) {
|
|
return onlySuites.indexOf(childSuite) !== -1 || childSuite.filterOnly();
|
|
});
|
|
}
|
|
// Keep the suite only if there is something to run
|
|
return this.tests.length > 0 || this.suites.length > 0;
|
|
};
|
|
|
|
/**
|
|
* Adds a suite to the list of subsuites marked `only`.
|
|
*
|
|
* @private
|
|
* @param {Suite} suite
|
|
*/
|
|
Suite.prototype.appendOnlySuite = function(suite) {
|
|
this._onlySuites.push(suite);
|
|
};
|
|
|
|
/**
|
|
* Marks a suite to be `only`.
|
|
*
|
|
* @private
|
|
*/
|
|
Suite.prototype.markOnly = function() {
|
|
this.parent && this.parent.appendOnlySuite(this);
|
|
};
|
|
|
|
/**
|
|
* Adds a test to the list of tests marked `only`.
|
|
*
|
|
* @private
|
|
* @param {Test} test
|
|
*/
|
|
Suite.prototype.appendOnlyTest = function(test) {
|
|
this._onlyTests.push(test);
|
|
};
|
|
|
|
/**
|
|
* Returns the array of hooks by hook name; see `HOOK_TYPE_*` constants.
|
|
* @private
|
|
*/
|
|
Suite.prototype.getHooks = function getHooks(name) {
|
|
return this['_' + name];
|
|
};
|
|
|
|
/**
|
|
* cleans all references from this suite and all child suites.
|
|
*/
|
|
Suite.prototype.dispose = function() {
|
|
this.suites.forEach(function(suite) {
|
|
suite.dispose();
|
|
});
|
|
this.cleanReferences();
|
|
};
|
|
|
|
/**
|
|
* Cleans up the references to all the deferred functions
|
|
* (before/after/beforeEach/afterEach) and tests of a Suite.
|
|
* These must be deleted otherwise a memory leak can happen,
|
|
* as those functions may reference variables from closures,
|
|
* thus those variables can never be garbage collected as long
|
|
* as the deferred functions exist.
|
|
*
|
|
* @private
|
|
*/
|
|
Suite.prototype.cleanReferences = function cleanReferences() {
|
|
function cleanArrReferences(arr) {
|
|
for (var i = 0; i < arr.length; i++) {
|
|
delete arr[i].fn;
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(this._beforeAll)) {
|
|
cleanArrReferences(this._beforeAll);
|
|
}
|
|
|
|
if (Array.isArray(this._beforeEach)) {
|
|
cleanArrReferences(this._beforeEach);
|
|
}
|
|
|
|
if (Array.isArray(this._afterAll)) {
|
|
cleanArrReferences(this._afterAll);
|
|
}
|
|
|
|
if (Array.isArray(this._afterEach)) {
|
|
cleanArrReferences(this._afterEach);
|
|
}
|
|
|
|
for (var i = 0; i < this.tests.length; i++) {
|
|
delete this.tests[i].fn;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns an object suitable for IPC.
|
|
* Functions are represented by keys beginning with `$$`.
|
|
* @private
|
|
* @returns {Object}
|
|
*/
|
|
Suite.prototype.serialize = function serialize() {
|
|
return {
|
|
_bail: this._bail,
|
|
$$fullTitle: this.fullTitle(),
|
|
$$isPending: this.isPending(),
|
|
root: this.root,
|
|
title: this.title,
|
|
id: this.id,
|
|
parent: this.parent ? {[MOCHA_ID_PROP_NAME]: this.parent.id} : null
|
|
};
|
|
};
|
|
|
|
var constants = defineConstants(
|
|
/**
|
|
* {@link Suite}-related constants.
|
|
* @public
|
|
* @memberof Suite
|
|
* @alias constants
|
|
* @readonly
|
|
* @static
|
|
* @enum {string}
|
|
*/
|
|
{
|
|
/**
|
|
* Event emitted after a test file has been loaded Not emitted in browser.
|
|
*/
|
|
EVENT_FILE_POST_REQUIRE: 'post-require',
|
|
/**
|
|
* Event emitted before a test file has been loaded. In browser, this is emitted once an interface has been selected.
|
|
*/
|
|
EVENT_FILE_PRE_REQUIRE: 'pre-require',
|
|
/**
|
|
* Event emitted immediately after a test file has been loaded. Not emitted in browser.
|
|
*/
|
|
EVENT_FILE_REQUIRE: 'require',
|
|
/**
|
|
* Event emitted when `global.run()` is called (use with `delay` option)
|
|
*/
|
|
EVENT_ROOT_SUITE_RUN: 'run',
|
|
|
|
/**
|
|
* Namespace for collection of a `Suite`'s "after all" hooks
|
|
*/
|
|
HOOK_TYPE_AFTER_ALL: 'afterAll',
|
|
/**
|
|
* Namespace for collection of a `Suite`'s "after each" hooks
|
|
*/
|
|
HOOK_TYPE_AFTER_EACH: 'afterEach',
|
|
/**
|
|
* Namespace for collection of a `Suite`'s "before all" hooks
|
|
*/
|
|
HOOK_TYPE_BEFORE_ALL: 'beforeAll',
|
|
/**
|
|
* Namespace for collection of a `Suite`'s "before all" hooks
|
|
*/
|
|
HOOK_TYPE_BEFORE_EACH: 'beforeEach',
|
|
|
|
// the following events are all deprecated
|
|
|
|
/**
|
|
* Emitted after an "after all" `Hook` has been added to a `Suite`. Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_HOOK_AFTER_ALL: 'afterAll',
|
|
/**
|
|
* Emitted after an "after each" `Hook` has been added to a `Suite` Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_HOOK_AFTER_EACH: 'afterEach',
|
|
/**
|
|
* Emitted after an "before all" `Hook` has been added to a `Suite` Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_HOOK_BEFORE_ALL: 'beforeAll',
|
|
/**
|
|
* Emitted after an "before each" `Hook` has been added to a `Suite` Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_HOOK_BEFORE_EACH: 'beforeEach',
|
|
/**
|
|
* Emitted after a child `Suite` has been added to a `Suite`. Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_SUITE: 'suite',
|
|
/**
|
|
* Emitted after a `Test` has been added to a `Suite`. Deprecated
|
|
*/
|
|
EVENT_SUITE_ADD_TEST: 'test'
|
|
}
|
|
);
|
|
|
|
/**
|
|
* @summary There are no known use cases for these events.
|
|
* @desc This is a `Set`-like object having all keys being the constant's string value and the value being `true`.
|
|
* @todo Remove eventually
|
|
* @type {Object<string,boolean>}
|
|
* @ignore
|
|
*/
|
|
var deprecatedEvents = Object.keys(constants)
|
|
.filter(function(constant) {
|
|
return constant.substring(0, 15) === 'EVENT_SUITE_ADD';
|
|
})
|
|
.reduce(function(acc, constant) {
|
|
acc[constants[constant]] = true;
|
|
return acc;
|
|
}, createMap());
|
|
|
|
Suite.constants = constants;
|