'use strict'; /** * @module Suite */ /** * 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'); /** * Expose `Suite`. */ exports = module.exports = Suite; /** * Create a new `Suite` with the given `title` and parent `Suite`. When a suite * with the same title is already present, that suite is returned to provide * nicer reporter and more flexible meta-testing. * * @memberof Mocha * @public * @api public * @param {Suite} parent * @param {string} title * @return {Suite} */ exports.create = function (parent, title) { var suite = new Suite(title, parent.ctx); suite.parent = parent; title = suite.fullTitle(); parent.addSuite(suite); return suite; }; /** * Initialize a new `Suite` with the given `title` and `ctx`. Derived from [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) * * @memberof Mocha * @public * @class * @param {string} title * @param {Context} parentContext */ function Suite (title, parentContext) { if (!utils.isString(title)) { throw new Error('Suite `title` should be a "string" but "' + typeof title + '" was given instead.'); } 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 = !title; this._timeout = 2000; this._enableTimeouts = true; this._slow = 75; this._bail = false; this._retries = -1; this._onlyTests = []; this._onlySuites = []; this.delayed = false; } /** * Inherit from `EventEmitter.prototype`. */ inherits(Suite, EventEmitter); /** * Return a clone of this `Suite`. * * @api private * @return {Suite} */ Suite.prototype.clone = function () { var suite = new Suite(this.title); debug('clone'); suite.ctx = this.ctx; 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". * * @api private * @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. * * @api 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`. * * @api 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". * * @api 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. * * @api 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. * * @api 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. * * @api 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('beforeAll', hook); return this; }; /** * Run `fn(test[, done])` after running tests. * * @api 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('afterAll', hook); return this; }; /** * Run `fn(test[, done])` before each test case. * * @api 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('beforeEach', hook); return this; }; /** * Run `fn(test[, done])` after each test case. * * @api 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('afterEach', hook); return this; }; /** * Add a test `suite`. * * @api private * @param {Suite} suite * @return {Suite} for chaining */ Suite.prototype.addSuite = function (suite) { suite.parent = this; 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('suite', suite); return this; }; /** * Add a `test` to this suite. * * @api 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('test', test); return this; }; /** * Return the full title generated by recursively concatenating the parent's * full title. * * @memberof Mocha.Suite * @public * @api 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 Mocha.Suite * @public * @api 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 Mocha.Suite * @public * @api 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)`. * * @api 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. */ Suite.prototype.run = function run () { if (this.root) { this.emit('run'); } };