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