| 'use strict'; |
| |
| /** |
| * Module dependencies. |
| */ |
| var EventEmitter = require('events').EventEmitter; |
| var Hook = require('./hook'); |
| var utils = require('./utils'); |
| var inherits = utils.inherits; |
| var debug = require('debug')('mocha:suite'); |
| var milliseconds = require('ms'); |
| var errors = require('./errors'); |
| var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError; |
| |
| /** |
| * 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 (!utils.isString(title)) { |
| throw 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.pending = false; |
| this._beforeEach = []; |
| this._beforeAll = []; |
| this._afterEach = []; |
| this._afterAll = []; |
| this.root = isRoot === true; |
| this._timeout = 2000; |
| this._enableTimeouts = true; |
| this._slow = 75; |
| this._bail = false; |
| this._retries = -1; |
| this._onlyTests = []; |
| this._onlySuites = []; |
| this.delayed = false; |
| |
| this.on('newListener', function(event) { |
| if (deprecatedEvents[event]) { |
| utils.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); |
| |
| /** |
| * 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.enableTimeouts(this.enableTimeouts()); |
| 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 (ms.toString() === '0') { |
| this._enableTimeouts = false; |
| } |
| if (typeof ms === 'string') { |
| ms = milliseconds(ms); |
| } |
| 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 timeout to `enabled`. |
| * |
| * @private |
| * @param {boolean} enabled |
| * @return {Suite|boolean} self or enabled |
| */ |
| Suite.prototype.enableTimeouts = function(enabled) { |
| if (!arguments.length) { |
| return this._enableTimeouts; |
| } |
| debug('enableTimeouts %s', enabled); |
| this._enableTimeouts = enabled; |
| 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.enableTimeouts(this.enableTimeouts()); |
| 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.enableTimeouts(this.enableTimeouts()); |
| 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.enableTimeouts(this.enableTimeouts()); |
| 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); |
| }; |
| |
| /** |
| * 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 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; |
| } |
| }; |
| |
| var constants = utils.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; |
| }, utils.createMap()); |
| |
| Suite.constants = constants; |