| 'use strict'; |
| |
| /** |
| * Module dependencies. |
| * @private |
| */ |
| const {EventEmitter} = require('events'); |
| const Hook = require('./hook'); |
| var { |
| assignNewMochaID, |
| clamp, |
| constants: utilsConstants, |
| 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(); |
| } |
| |
| /** |
| * 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 hook; |
| }; |
| |
| /** |
| * 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 hook; |
| }; |
| |
| /** |
| * 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 hook; |
| }; |
| |
| /** |
| * 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 hook; |
| }; |
| |
| /** |
| * 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: Boolean(this.isPending()), |
| root: this.root, |
| title: this.title, |
| [MOCHA_ID_PROP_NAME]: 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 each" hooks. |
| */ |
| HOOK_TYPE_BEFORE_EACH: 'beforeEach', |
| |
| /** |
| * Emitted after a child `Suite` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_SUITE: 'suite', |
| /** |
| * Emitted after an "after all" `Hook` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_HOOK_AFTER_ALL: 'afterAll', |
| /** |
| * Emitted after an "after each" `Hook` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_HOOK_AFTER_EACH: 'afterEach', |
| /** |
| * Emitted after an "before all" `Hook` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_HOOK_BEFORE_ALL: 'beforeAll', |
| /** |
| * Emitted after an "before each" `Hook` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_HOOK_BEFORE_EACH: 'beforeEach', |
| /** |
| * Emitted after a `Test` has been added to a `Suite`. |
| */ |
| EVENT_SUITE_ADD_TEST: 'test' |
| } |
| ); |
| |
| Suite.constants = constants; |