Spaces:
Running
Running
; | |
/** | |
* 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; | |