/** * 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. * * @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`. * * @api private * @param {string} title * @param {Context} parentContext */ function Suite(title, parentContext) { 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.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 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 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 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 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; }; /** * Sets 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()); }; /** * 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 = 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; 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 = 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; 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 = 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; 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 = 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; 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. * * @api public * @return {string} */ Suite.prototype.fullTitle = function() { if (this.parent) { var full = this.parent.fullTitle(); if (full) { return full + ' ' + this.title; } } return this.title; }; /** * Return the total number of tests. * * @api public * @return {number} */ Suite.prototype.total = function() { return utils.reduce(this.suites, 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) { utils.forEach(this.tests, fn); utils.forEach(this.suites, 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'); } };