diff --git a/.eslintignore b/.eslintignore index 48c498f279..2226cf1fc2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ coverage/ mocha.js *.fixture.js docs/ +out/ !lib/mocha.js diff --git a/.gitignore b/.gitignore index f11fc29c86..7e1edeb748 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ yarn.lock docs/_site docs/_dist docs/api +out/ .vscode/ diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..35cee72dcb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.4.3 diff --git a/.travis.yml b/.travis.yml index 94aa85770e..f6153f6815 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +### +### .travis.yml +### + # these are executed in order. each must pass for the next to be run stages: - smoke # this ensures a "user" install works properly @@ -7,7 +11,11 @@ stages: # defaults language: node_js -node_js: '10' +node_js: '11' +addons: + apt: + packages: + - libnotify-bin # `nvm install` happens before the cache is restored, which means # we must install our own npm elsewhere (`~/npm`) before_install: | @@ -32,7 +40,7 @@ jobs: - &node script: npm start test.node - node_js: '9' + node_js: '10' - <<: *node node_js: '8' @@ -67,7 +75,7 @@ jobs: - node_modules # npm install, unlike npm ci, doesn't wipe node_modules - <<: *smoke - node_js: '9' + node_js: '10' - <<: *smoke node_js: '8' diff --git a/Gemfile b/Gemfile index 743d18eca7..1a8c10648c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" # Added at 2017-12-05 23:01:55 -0800 by boneskull: -gem "jekyll", "~> 3.6" +gem "jekyll", ">= 3.8.4" diff --git a/Gemfile.lock b/Gemfile.lock index ad15098a0c..dd897f793e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GEM http_parser.rb (0.6.0) i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.8.3) + jekyll (3.8.4) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -29,25 +29,25 @@ GEM safe_yaml (~> 1.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) - jekyll-watch (2.0.0) + jekyll-watch (2.1.2) listen (~> 3.0) kramdown (1.17.0) - liquid (4.0.0) + liquid (4.0.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) mercenary (0.3.6) - pathutil (0.16.1) + pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (3.0.3) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - rouge (3.2.1) + rouge (3.3.0) ruby_dep (1.5.0) safe_yaml (1.0.4) - sass (3.5.7) + sass (3.6.0) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -57,7 +57,7 @@ PLATFORMS ruby DEPENDENCIES - jekyll (~> 3.6) + jekyll (>= 3.8.4) BUNDLED WITH - 1.16.1 + 1.16.6 diff --git a/MAINTAINERS.md b/MAINTAINERS.md index bee04f42d1..55e5436cfc 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -279,18 +279,16 @@ This is not necessarily ideal, and we should consider another method of using br 1. Decide whether this is a `patch`, `minor`, or `major` release by the PRs which have been merged since the last release. 1. Checkout `master` in your working copy & pull. 1. Modify `CHANGELOG.md`; follow the existing conventions in that file. Commit this file only; add `[ci skip]` to the commit message to avoid a build. -1. Use `npm version` to bump the version; see `npm version --help` for more info. (Hint--use `-m`: e.g. `npm version patch -m 'Release v%s'`) -1. Push `master` to origin with your new tag; e.g. `git push origin master --tags` +1. Use `npm version` (use `npm@6+`) to bump the version; see `npm version --help` for more info. (Hint--use `-m`: e.g., `npm version patch -m 'Release v%s'`) +1. Push `master` to `origin` with your new tag; e.g. `git push origin master --tags` 1. Copy & paste the added lines to a new GitHub "release". Be sure to add any missing link references (use "preview" button). Save release as draft. 1. Meanwhile, you can check [the build](https://travis-ci.org/mochajs/mocha) on Travis-CI. -1. Once it's green and you're satisfied with the release notes, open your draft release on GitHub, then click "publish" +1. Once the build is green, and you're satisfied with the release notes, open your draft release on GitHub, then click "publish." 1. Back in your working copy, run `npm publish`. -1. Announce the update on Twitter or just tell your dog or something. +1. Announce the update on Twitter or just tell your dog or something. New releases will be automatically tweeted by [@b0neskull](https://twitter.com/b0neskull). *Note: there are too many steps above.* -> As of this writing, `npm version` (using npm@5) is not working well, and you may have to tag manually. Commit the change to the version in `package.json` with a message of the format `Release vX.Y.Z`, then tag the changeset using `vX.Y.Z`. - ## Projects There are [Projects](https://github.com/mochajs/mocha/projects), but we haven't yet settled on how to use them. diff --git a/appveyor.yml b/appveyor.yml index 2d2e1e459d..e9f8eba0c0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,27 +1,71 @@ -platform: - - x64 +### +### appveyor.yml +### + +## General configuration +version: '{build}' +skip_commits: + message: /\[ci\s+skip\]/ + +## Environment configuration +shallow_clone: true +clone_depth: 1 environment: matrix: + - nodejs_version: '11' - nodejs_version: '10' - - nodejs_version: '9' - nodejs_version: '8' - nodejs_version: '6' +matrix: + fast_finish: true install: + ## Manual Growl install + - ps: Add-AppveyorMessage "Installing Growl..." + - ps: $exePath = "$($env:USERPROFILE)\GrowlInstaller.exe" + - ps: (New-Object Net.WebClient).DownloadFile('http://www.growlforwindows.com/gfw/downloads/GrowlInstaller.exe', $exePath) + - ps: mkdir C:\GrowlInstaller | out-null + - ps: 7z x $exePath -oC:\GrowlInstaller | out-null + - ps: cmd /c start /wait msiexec /i C:\GrowlInstaller\Growl_v2.0.msi /quiet + - ps: $env:path = "C:\Program Files (x86)\Growl for Windows;$env:path" + ## Node-related installs + - ps: Add-AppveyorMessage "Installing Node..." + - set PATH=%APPDATA%\npm;C:\MinGW\bin;%PATH% - ps: Install-Product node $env:nodejs_version x64 - - set CI=true - - set PATH=%APPDATA%\npm;c:\MinGW\bin;%PATH% + - ps: Add-AppveyorMessage "Installing npm..." - npm install -g npm@^5 + ## Mocha-related package installs + - ps: Add-AppveyorMessage "Installing Mocha dependencies..." - npm ci --ignore-scripts -matrix: - fast_finish: true -build: off -version: '{build}' -shallow_clone: true -clone_depth: 1 + +## Build configuration +platform: + - x64 +build: script +before_build: + ## Growl requires some time before it's ready to handle notifications + - ps: Start-Process -NoNewWindow Growl + - ps: Add-AppveyorMessage "Started Growl service..." + - ps: Start-Sleep -Milliseconds 2000 +build_script: + ## Placeholder command + - ps: Start-Sleep -Milliseconds 0 + #- ps: Add-AppveyorMessage "Verify Growl responding..." + #- ps: growlnotify test + +## Test configuration +before_test: + - set CI=true test_script: + - ps: Add-AppveyorMessage "Displaying version information" - node --version - npm --version + - ps: Add-AppveyorMessage "Running tests..." - npm start test.node -skip_commits: - message: /\[ci\s+skip\]/ + - ps: Add-AppveyorMessage "Done" +## Notifications +notifications: + - provider: Email + on_build_success: false + on_build_failure: false + on_build_status_changed: false diff --git a/bin/_mocha b/bin/_mocha index 41e9871b76..742c5d70d5 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -157,9 +157,17 @@ program .option('-f, --fgrep ', 'only run tests containing ') .option('-gc, --expose-gc', 'expose gc extension') .option('-i, --invert', 'inverts --grep and --fgrep matches') - .option('-r, --require ', 'require the given module') - .option('-s, --slow ', '"slow" test threshold in milliseconds [75]') - .option('-t, --timeout ', 'set test-case timeout in milliseconds [2000]') + .option('-r, --require ', 'require the given module', []) + .option( + '-s, --slow ', + 'specify "slow" test threshold in milliseconds', + 75 + ) + .option( + '-t, --timeout ', + 'specify test timeout threshold in milliseconds', + 2000 + ) .option( '-u, --ui ', `specify user-interface (${interfaceNames.join('|')})`, @@ -204,13 +212,16 @@ program '--inspect-brk', 'activate devtools in chrome and break on the first line' ) - .option('--interfaces', 'display available interfaces') + .option('--interfaces', 'output provided interfaces and exit') .option('--no-deprecation', 'silence deprecation warnings') .option( '--exit', 'force shutdown of the event loop after test run: mocha will call process.exit' ) - .option('--no-timeouts', 'disables timeouts, given implicitly with --debug') + .option( + '--no-timeouts', + 'disables timeouts, given implicitly with --debug/--inspect' + ) .option('--no-warnings', 'silence all node process warnings') .option('--opts ', 'specify opts path', 'test/mocha.opts') .option('--perf-basic-prof', 'enable perf linux profiler (basic support)') @@ -218,10 +229,11 @@ program .option('--prof', 'log statistical profiling information') .option('--log-timer-events', 'Time events including external callbacks') .option('--recursive', 'include sub directories') - .option('--reporters', 'display available reporters') + .option('--reporters', 'output provided reporters and exit') .option( '--retries ', - 'set numbers of time to retry a failed test case' + 'specify number of times to retry a failed test case', + 0 ) .option( '--throw-deprecation', @@ -246,11 +258,16 @@ program ) .option( '--file ', - 'include a file to be ran during the suite', + 'adds file be loaded prior to suite execution', collect, [] ) - .option('--exclude ', 'a file or glob pattern to ignore', collect, []); + .option( + '--exclude ', + 'adds file or glob pattern to ignore', + collect, + [] + ); program._name = 'mocha'; diff --git a/bin/options.js b/bin/options.js index a25a18e28e..0c27ae5fa9 100644 --- a/bin/options.js +++ b/bin/options.js @@ -13,9 +13,54 @@ const fs = require('fs'); module.exports = getOptions; /** - * Get options. + * Default pathname for run-control file. + * + * @constant + * @type {string} + * @default */ +const defaultPathname = 'test/mocha.opts'; +/** + * Reads contents of the run-control file. + * + * @private + * @param {string} pathname - Pathname of run-control file. + * @returns {string} file contents + */ +function readOptionsFile(pathname) { + return fs.readFileSync(pathname, 'utf8'); +} + +/** + * Parses options read from run-control file. + * + * @private + * @param {string} content - Content read from run-control file. + * @returns {string[]} cmdline options (and associated arguments) + */ +function parseOptions(content) { + /* + * Replaces comments with empty strings + * Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space + * Splits on whitespace, creating array of substrings + * Filters empty string elements from array + * Replaces any HTML space with space + */ + return content + .replace(/^#.*$/gm, '') + .replace(/\\\s/g, '%20') + .split(/\s/) + .filter(Boolean) + .map(value => value.replace(/%20/g, ' ')); +} + +/** + * Prepends options from run-control file to the command line arguments. + * + * @public + * @see {@link https://mochajs.org/#mochaopts|mocha.opts} + */ function getOptions() { if ( process.argv.length === 3 && @@ -26,17 +71,11 @@ function getOptions() { const optsPath = process.argv.indexOf('--opts') === -1 - ? 'test/mocha.opts' + ? defaultPathname : process.argv[process.argv.indexOf('--opts') + 1]; try { - const opts = fs - .readFileSync(optsPath, 'utf8') - .replace(/^#.*$/gm, '') - .replace(/\\\s/g, '%20') - .split(/\s/) - .filter(Boolean) - .map(value => value.replace(/%20/g, ' ')); + const opts = parseOptions(readOptionsFile(optsPath)); process.argv = process.argv .slice(0, 2) diff --git a/docs/index.md b/docs/index.md index 066ee691ae..67376c4a35 100644 --- a/docs/index.md +++ b/docs/index.md @@ -780,73 +780,71 @@ Mocha supports the `err.expected` and `err.actual` properties of any thrown `Ass ## Usage -```text - Usage: mocha [debug] [options] [files] - - Options: - - -V, --version output the version number - -A, --async-only force all tests to take a callback (async) or return a promise - -c, --colors force enabling of colors - -C, --no-colors force disabling of colors - -G, --growl enable Growl notification support - -O, --reporter-options reporter-specific options - -R, --reporter specify the reporter to use (default: spec) - -S, --sort sort test files - -b, --bail bail after first test failure - -d, --debug enable node's debugger, synonym for node --debug - -g, --grep only run tests matching - -f, --fgrep only run tests containing - -gc, --expose-gc expose gc extension - -i, --invert inverts --grep and --fgrep matches - -r, --require require the given module - -s, --slow "slow" test threshold in milliseconds [75] - -t, --timeout set test-case timeout in milliseconds [2000] - -u, --ui specify user-interface (bdd|tdd|qunit|exports) (default: bdd) - -w, --watch watch files in the current working directory for changes - --check-leaks check for global variable leaks - --full-trace display the full stack trace - --compilers :,... use the given module(s) to compile files (default: ) - --debug-brk enable node's debugger breaking on the first line - --globals allow the given comma-delimited global [names] (default: ) - --es_staging enable all staged features - --harmony<_classes,_generators,...> all node --harmony* flags are available - --preserve-symlinks Instructs the module loader to preserve symbolic links when resolving and caching modules - --icu-data-dir include ICU data - --inline-diffs display actual/expected differences inline within each string - --no-diff do not show a diff on failure - --inspect activate devtools in chrome - --inspect-brk activate devtools in chrome and break on the first line - --interfaces display available interfaces - --no-deprecation silence deprecation warnings - --exit force shutdown of the event loop after test run: mocha will call process.exit - --no-timeouts disables timeouts, given implicitly with --debug - --no-warnings silence all node process warnings - --opts specify opts path (default: test/mocha.opts) - --perf-basic-prof enable perf linux profiler (basic support) - --napi-modules enable experimental NAPI modules - --prof log statistical profiling information - --log-timer-events Time events including external callbacks - --recursive include sub directories - --reporters display available reporters - --retries set numbers of time to retry a failed test case - --throw-deprecation throw an exception anytime a deprecated function is used - --trace trace function calls - --trace-deprecation show stack traces on deprecations - --trace-warnings show stack traces on node process warnings - --use_strict enforce strict mode - --watch-extensions ,... specify extensions to monitor with --watch (default: js) - --delay wait for async suite definition - --allow-uncaught enable uncaught errors to propagate - --forbid-only causes test marked with only to fail the suite - --forbid-pending causes pending tests and test marked with skip to fail the suite - --file include a file to be ran during the suite (default: ) - --exclude a file or glob pattern to ignore (default: ) - -h, --help output usage information - - Commands: - - init initialize a client-side mocha setup at +```console +Usage: mocha [debug] [options] [files] + +Options: + -V, --version output the version number + -A, --async-only force all tests to take a callback (async) or return a promise + -c, --colors force enabling of colors + -C, --no-colors force disabling of colors + -G, --growl enable growl notification support + -O, --reporter-options reporter-specific options + -R, --reporter specify the reporter to use (default: "spec") + -S, --sort sort test files + -b, --bail bail after first test failure + -d, --debug enable node's debugger, synonym for node --debug + -g, --grep only run tests matching + -f, --fgrep only run tests containing + -gc, --expose-gc expose gc extension + -i, --invert inverts --grep and --fgrep matches + -r, --require require the given module (default: []) + -s, --slow specify "slow" test threshold in milliseconds (default: 75) + -t, --timeout specify test timeout threshold in milliseconds (default: 2000) + -u, --ui specify user-interface (bdd|tdd|qunit|exports) (default: "bdd") + -w, --watch watch files in the current working directory for changes + --check-leaks check for global variable leaks + --full-trace display the full stack trace + --compilers :,... use the given module(s) to compile files (default: []) + --debug-brk enable node's debugger breaking on the first line + --globals allow the given comma-delimited global [names] (default: []) + --es_staging enable all staged features + --harmony<_classes,_generators,...> all node --harmony* flags are available + --preserve-symlinks Instructs the module loader to preserve symbolic links when resolving and caching modules + --icu-data-dir include ICU data + --inline-diffs display actual/expected differences inline within each string + --no-diff do not show a diff on failure + --inspect activate devtools in chrome + --inspect-brk activate devtools in chrome and break on the first line + --interfaces output provided interfaces and exit + --no-deprecation silence deprecation warnings + --exit force shutdown of the event loop after test run: mocha will call process.exit + --no-timeouts disables timeouts, given implicitly with --debug/--inspect + --no-warnings silence all node process warnings + --opts specify opts path (default: "test/mocha.opts") + --perf-basic-prof enable perf linux profiler (basic support) + --napi-modules enable experimental NAPI modules + --prof log statistical profiling information + --log-timer-events Time events including external callbacks + --recursive include sub directories + --reporters output provided reporters and exit + --retries specify number of times to retry a failed test case (default: 0) + --throw-deprecation throw an exception anytime a deprecated function is used + --trace trace function calls + --trace-deprecation show stack traces on deprecations + --trace-warnings show stack traces on node process warnings + --use_strict enforce strict mode + --watch-extensions ,... specify extensions to monitor with --watch (default: ["js"]) + --delay wait for async suite definition + --allow-uncaught enable uncaught errors to propagate + --forbid-only causes test marked with only to fail the suite + --forbid-pending causes pending tests and test marked with skip to fail the suite + --file adds file be loaded prior to suite execution (default: []) + --exclude adds file or glob pattern to ignore (default: []) + -h, --help output usage information + +Commands: + init initialize a client-side mocha setup at ``` ### `-w, --watch` diff --git a/lib/interfaces/common.js b/lib/interfaces/common.js index 4ca340a608..7997312786 100644 --- a/lib/interfaces/common.js +++ b/lib/interfaces/common.js @@ -1,6 +1,7 @@ 'use strict'; var Suite = require('../suite'); +var utils = require('../utils'); /** * Functions common to more than one interface. @@ -92,6 +93,7 @@ module.exports = function(suites, context, mocha) { /** * Creates a suite. + * * @param {Object} opts Options * @param {string} opts.title Title of Suite * @param {Function} [opts.fn] Suite Function (not always applicable) @@ -109,13 +111,22 @@ module.exports = function(suites, context, mocha) { suite.parent._onlySuites = suite.parent._onlySuites.concat(suite); } if (typeof opts.fn === 'function') { - opts.fn.call(suite); + var result = opts.fn.call(suite); + if (typeof result !== 'undefined') { + utils.deprecate( + 'Deprecation Warning: Suites do not support a return value;' + + opts.title + + ' returned :' + + result + ); + } suites.shift(); } else if (typeof opts.fn === 'undefined' && !suite.pending) { throw new Error( 'Suite "' + suite.fullTitle() + - '" was defined but no callback was supplied. Supply a callback or explicitly skip the suite.' + '" was defined but no callback was supplied. ' + + 'Supply a callback or explicitly skip the suite.' ); } else if (!opts.fn && suite.pending) { suites.shift(); diff --git a/lib/mocha.js b/lib/mocha.js index dabea27e00..3fd8d6cafa 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -675,6 +675,20 @@ Mocha.prototype.forbidPending = function() { return this; }; +/** + * Mocha version as specified by "package.json". + * + * @name Mocha#version + * @type string + * @readonly + */ +Object.defineProperty(Mocha.prototype, 'version', { + value: require('../package').version, + configurable: false, + enumerable: true, + writable: false +}); + /** * Callback to be invoked when test execution is complete. * diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js index 0edd0cbf88..5b4cfbcebe 100644 --- a/lib/reporters/json-stream.js +++ b/lib/reporters/json-stream.js @@ -9,55 +9,68 @@ var Base = require('./base'); /** - * Expose `List`. + * Expose `JSONStream`. */ -exports = module.exports = List; +exports = module.exports = JSONStream; /** - * Initialize a new `JSONStream` test reporter. + * Constructs a new `JSONStream` reporter instance. * * @public - * @name JSONStream - * @class JSONStream - * @memberof Mocha.reporters + * @class * @extends Mocha.reporters.Base - * @api public - * @param {Runner} runner + * @memberof Mocha.reporters + * @param {Runner} runner - Instance triggers reporter actions. */ -function List(runner) { +function JSONStream(runner) { Base.call(this, runner); var self = this; var total = runner.total; - runner.on('start', function() { - console.log(JSON.stringify(['start', {total: total}])); + runner.once('start', function() { + writeEvent(['start', {total: total}]); }); runner.on('pass', function(test) { - console.log(JSON.stringify(['pass', clean(test)])); + writeEvent(['pass', clean(test)]); }); runner.on('fail', function(test, err) { test = clean(test); test.err = err.message; test.stack = err.stack || null; - console.log(JSON.stringify(['fail', test])); + writeEvent(['fail', test]); }); runner.once('end', function() { - process.stdout.write(JSON.stringify(['end', self.stats])); + writeEvent(['end', self.stats]); }); } /** - * Return a plain-object representation of `test` - * free of cyclic properties etc. + * Mocha event to be written to the output stream. + * @typedef {Array} JSONStream~MochaEvent + */ + +/** + * Writes Mocha event to reporter output stream. + * + * @private + * @param {JSONStream~MochaEvent} event - Mocha event to be output. + */ +function writeEvent(event) { + process.stdout.write(JSON.stringify(event) + '\n'); +} + +/** + * Returns an object literal representation of `test` + * free of cyclic properties, etc. * - * @api private - * @param {Object} test - * @return {Object} + * @private + * @param {Test} test - Instance used as data source. + * @return {Object} object containing pared-down test instance data */ function clean(test) { return { diff --git a/lib/reporters/tap.js b/lib/reporters/tap.js index feaf7ea21a..ccff657e6a 100644 --- a/lib/reporters/tap.js +++ b/lib/reporters/tap.js @@ -6,7 +6,10 @@ * Module dependencies. */ +var util = require('util'); var Base = require('./base'); +var inherits = require('../utils').inherits; +var sprintf = util.format; /** * Expose `TAP`. @@ -15,25 +18,34 @@ var Base = require('./base'); exports = module.exports = TAP; /** - * Initialize a new `TAP` reporter. + * Constructs a new TAP reporter with runner instance and reporter options. * * @public * @class - * @memberof Mocha.reporters * @extends Mocha.reporters.Base - * @api public - * @param {Runner} runner + * @memberof Mocha.reporters + * @param {Runner} runner - Instance triggers reporter actions. + * @param {Object} [options] - runner options */ -function TAP(runner) { - Base.call(this, runner); +function TAP(runner, options) { + Base.call(this, runner, options); + var self = this; var n = 1; - var passes = 0; - var failures = 0; - runner.on('start', function() { - var total = runner.grepTotal(runner.suite); - console.log('%d..%d', 1, total); + var tapVersion = '12'; + if (options && options.reporterOptions) { + if (options.reporterOptions.tapVersion) { + tapVersion = options.reporterOptions.tapVersion.toString(); + } + } + + this._producer = createProducer(tapVersion); + + runner.once('start', function() { + var ntests = runner.grepTotal(runner.suite); + self._producer.writeVersion(); + self._producer.writePlan(ntests); }); runner.on('test end', function() { @@ -41,39 +53,233 @@ function TAP(runner) { }); runner.on('pending', function(test) { - console.log('ok %d %s # SKIP -', n, title(test)); + self._producer.writePending(n, test); }); runner.on('pass', function(test) { - passes++; - console.log('ok %d %s', n, title(test)); + self._producer.writePass(n, test); }); runner.on('fail', function(test, err) { - failures++; - console.log('not ok %d %s', n, title(test)); - if (err.message) { - console.log(err.message.replace(/^/gm, ' ')); - } - if (err.stack) { - console.log(err.stack.replace(/^/gm, ' ')); - } + self._producer.writeFail(n, test, err); }); runner.once('end', function() { - console.log('# tests ' + (passes + failures)); - console.log('# pass ' + passes); - console.log('# fail ' + failures); + self._producer.writeEpilogue(runner.stats); }); } /** - * Return a TAP-safe title of `test` + * Inherit from `Base.prototype`. + */ +inherits(TAP, Base); + +/** + * Returns a TAP-safe title of `test`. * - * @api private - * @param {Object} test - * @return {String} + * @private + * @param {Test} test - Test instance. + * @return {String} title with any hash character removed */ function title(test) { return test.fullTitle().replace(/#/g, ''); } + +/** + * Writes newline-terminated formatted string to reporter output stream. + * + * @private + * @param {string} format - `printf`-like format string + * @param {...*} [varArgs] - Format string arguments + */ +function println(format, varArgs) { + var vargs = Array.from(arguments); + vargs[0] += '\n'; + process.stdout.write(sprintf.apply(null, vargs)); +} + +/** + * Returns a `tapVersion`-appropriate TAP producer instance, if possible. + * + * @private + * @param {string} tapVersion - Version of TAP specification to produce. + * @returns {TAPProducer} specification-appropriate instance + * @throws {Error} if specification version has no associated producer. + */ +function createProducer(tapVersion) { + var producers = { + '12': new TAP12Producer(), + '13': new TAP13Producer() + }; + var producer = producers[tapVersion]; + + if (!producer) { + throw new Error( + 'invalid or unsupported TAP version: ' + JSON.stringify(tapVersion) + ); + } + + return producer; +} + +/** + * @summary + * Constructs a new TAPProducer. + * + * @description + * Only to be used as an abstract base class. + * + * @private + * @constructor + */ +function TAPProducer() {} + +/** + * Writes the TAP version to reporter output stream. + * + * @abstract + */ +TAPProducer.prototype.writeVersion = function() {}; + +/** + * Writes the plan to reporter output stream. + * + * @abstract + * @param {number} ntests - Number of tests that are planned to run. + */ +TAPProducer.prototype.writePlan = function(ntests) { + println('%d..%d', 1, ntests); +}; + +/** + * Writes that test passed to reporter output stream. + * + * @abstract + * @param {number} n - Index of test that passed. + * @param {Test} test - Instance containing test information. + */ +TAPProducer.prototype.writePass = function(n, test) { + println('ok %d %s', n, title(test)); +}; + +/** + * Writes that test was skipped to reporter output stream. + * + * @abstract + * @param {number} n - Index of test that was skipped. + * @param {Test} test - Instance containing test information. + */ +TAPProducer.prototype.writePending = function(n, test) { + println('ok %d %s # SKIP -', n, title(test)); +}; + +/** + * Writes that test failed to reporter output stream. + * + * @abstract + * @param {number} n - Index of test that failed. + * @param {Test} test - Instance containing test information. + * @param {Error} err - Reason the test failed. + */ +TAPProducer.prototype.writeFail = function(n, test, err) { + println('not ok %d %s', n, title(test)); +}; + +/** + * Writes the summary epilogue to reporter output stream. + * + * @abstract + * @param {Object} stats - Object containing run statistics. + */ +TAPProducer.prototype.writeEpilogue = function(stats) { + // :TBD: Why is this not counting pending tests? + println('# tests ' + (stats.passes + stats.failures)); + println('# pass ' + stats.passes); + // :TBD: Why are we not showing pending results? + println('# fail ' + stats.failures); +}; + +/** + * @summary + * Constructs a new TAP12Producer. + * + * @description + * Produces output conforming to the TAP12 specification. + * + * @private + * @constructor + * @extends TAPProducer + * @see {@link https://testanything.org/tap-specification.html|Specification} + */ +function TAP12Producer() { + /** + * Writes that test failed to reporter output stream, with error formatting. + * @override + */ + this.writeFail = function(n, test, err) { + TAPProducer.prototype.writeFail.call(this, n, test, err); + if (err.message) { + println(err.message.replace(/^/gm, ' ')); + } + if (err.stack) { + println(err.stack.replace(/^/gm, ' ')); + } + }; +} + +/** + * Inherit from `TAPProducer.prototype`. + */ +inherits(TAP12Producer, TAPProducer); + +/** + * @summary + * Constructs a new TAP13Producer. + * + * @description + * Produces output conforming to the TAP13 specification. + * + * @private + * @constructor + * @extends TAPProducer + * @see {@link https://testanything.org/tap-version-13-specification.html|Specification} + */ +function TAP13Producer() { + /** + * Writes the TAP version to reporter output stream. + * @override + */ + this.writeVersion = function() { + println('TAP version 13'); + }; + + /** + * Writes that test failed to reporter output stream, with error formatting. + * @override + */ + this.writeFail = function(n, test, err) { + TAPProducer.prototype.writeFail.call(this, n, test, err); + var emitYamlBlock = err.message != null || err.stack != null; + if (emitYamlBlock) { + println(indent(1) + '---'); + if (err.message) { + println(indent(2) + 'message: |-'); + println(err.message.replace(/^/gm, indent(3))); + } + if (err.stack) { + println(indent(2) + 'stack: |-'); + println(err.stack.replace(/^/gm, indent(3))); + } + println(indent(1) + '...'); + } + }; + + function indent(level) { + return Array(level + 1).join(' '); + } +} + +/** + * Inherit from `TAPProducer.prototype`. + */ +inherits(TAP13Producer, TAPProducer); diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index c1a930d2d8..bd89a37e76 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -151,6 +151,8 @@ XUnit.prototype.write = function(line) { * @param {Test} test */ XUnit.prototype.test = function(test) { + Base.useColors = false; + var attrs = { classname: test.parent.fullTitle(), name: test.title, @@ -159,6 +161,10 @@ XUnit.prototype.test = function(test) { if (test.state === 'failed') { var err = test.err; + var diff = + Base.hideDiff || !err.actual || !err.expected + ? '' + : '\n' + Base.generateDiff(err.actual, err.expected); this.write( tag( 'testcase', @@ -168,7 +174,7 @@ XUnit.prototype.test = function(test) { 'failure', {}, false, - escape(err.message) + '\n' + escape(err.stack) + escape(err.message) + escape(diff) + '\n' + escape(err.stack) ) ) ); diff --git a/lib/runnable.js b/lib/runnable.js index 73da817793..2e62a6bc8b 100644 --- a/lib/runnable.js +++ b/lib/runnable.js @@ -50,23 +50,43 @@ function Runnable(title, fn) { utils.inherits(Runnable, EventEmitter); /** - * Set & get timeout `ms`. + * Get current timeout value in msecs. * - * @api private - * @param {number|string} ms - * @return {Runnable|number} ms or Runnable instance. + * @private + * @returns {number} current timeout threshold value + */ +/** + * @summary + * Set timeout threshold value (msecs). + * + * @description + * A string argument can use shorthand (e.g., "2s") and will be converted. + * The value will be clamped to range [0, 2^31-1]. + * If clamped value matches either range endpoint, timeouts will be disabled. + * + * @private + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value} + * @param {number|string} ms - Timeout threshold value. + * @returns {Runnable} this + * @chainable */ Runnable.prototype.timeout = function(ms) { if (!arguments.length) { return this._timeout; } - // see #1652 for reasoning - if (ms === 0 || ms > Math.pow(2, 31)) { - this._enableTimeouts = false; - } if (typeof ms === 'string') { ms = milliseconds(ms); } + + // Clamp to range + var INT_MAX = Math.pow(2, 31) - 1; + var range = [0, INT_MAX]; + ms = utils.clamp(ms, range); + + // see #1652 for reasoning + if (ms === range[0] || ms === range[1]) { + this._enableTimeouts = false; + } debug('timeout %d', ms); this._timeout = ms; if (this.timer) { diff --git a/lib/runner.js b/lib/runner.js index d105bca321..dc2092f9fa 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -851,10 +851,15 @@ Runner.prototype.run = function(fn) { filterOnly(rootSuite); } self.started = true; - self.emit('start'); + Runner.immediately(function() { + self.emit('start'); + }); + self.runSuite(rootSuite, function() { debug('finished running'); - self.emit('end'); + Runner.immediately(function() { + self.emit('end'); + }); }); } diff --git a/lib/utils.js b/lib/utils.js index e67bf74414..6e78a99b71 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -584,6 +584,34 @@ exports.getError = function(err) { return err || exports.undefinedError(); }; +/** + * Show a deprecation warning. Each distinct message is only displayed once. + * + * @param {string} msg + */ +exports.deprecate = function(msg) { + msg = String(msg); + if (seenDeprecationMsg.hasOwnProperty(msg)) { + return; + } + seenDeprecationMsg[msg] = true; + doDeprecationWarning(msg); +}; + +var seenDeprecationMsg = {}; + +var doDeprecationWarning = process.emitWarning + ? function(msg) { + // Node.js v6+ + process.emitWarning(msg, 'DeprecationWarning'); + } + : function(msg) { + // Everything else + process.nextTick(function() { + console.warn(msg); + }); + }; + /** * @summary * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) @@ -663,6 +691,17 @@ exports.isPromise = function isPromise(value) { return typeof value === 'object' && typeof value.then === 'function'; }; +/** + * Clamps a numeric value to an inclusive range. + * + * @param {number} value - Value to be clamped. + * @param {numer[]} range - Two element array specifying [min, max] range. + * @returns {number} clamped value + */ +exports.clamp = function clamp(value, range) { + return Math.min(Math.max(value, range[0]), range[1]); +}; + /** * It's a noop. * @api diff --git a/package-lock.json b/package-lock.json index 782bb42830..72c4a6f2ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1316,6 +1316,12 @@ "inherits": "^2.0.1" } }, + "browserify-package-json": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-package-json/-/browserify-package-json-1.0.1.tgz", + "integrity": "sha1-mN3oqlxWH9bT/km7qhArdLOW/eo=", + "dev": true + }, "browserify-rsa": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", @@ -2026,9 +2032,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "comment-regex": { "version": "1.0.1", @@ -5939,6 +5945,12 @@ "uglify-js": "3.3.x" }, "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13680,6 +13692,15 @@ "thunkify": "^2.1.2" } }, + "package-json-versionify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/package-json-versionify/-/package-json-versionify-1.0.4.tgz", + "integrity": "sha1-WGBYepRIc6a35tJujlH/siMVvxc=", + "dev": true, + "requires": { + "browserify-package-json": "^1.0.0" + } + }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -17954,6 +17975,12 @@ "source-map": "~0.6.1" }, "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 002b55e32b..7b1938ffa1 100644 --- a/package.json +++ b/package.json @@ -446,10 +446,19 @@ "type": "git", "url": "https://github.com/mochajs/mocha.git" }, + "bugs": { + "url": "https://github.com/mochajs/mocha/issues/" + }, + "homepage": "https://mochajs.org/", + "logo": "https://cldup.com/S9uQ-cOLYz.svg", "bin": { "mocha": "./bin/mocha", "_mocha": "./bin/_mocha" }, + "directories": { + "lib": "./lib", + "test": "./test" + }, "engines": { "node": ">= 6.0.0" }, @@ -461,7 +470,7 @@ }, "dependencies": { "browser-stdout": "1.3.1", - "commander": "2.15.1", + "commander": "2.19.0", "debug": "3.1.0", "diff": "3.5.0", "escape-string-regexp": "1.0.5", @@ -502,6 +511,7 @@ "markdownlint-cli": "^0.9.0", "nps": "^5.7.1", "nyc": "^11.7.3", + "package-json-versionify": "^1.0.4", "prettier-eslint-cli": "^4.7.1", "rimraf": "^2.5.2", "svgo": "^0.7.2", @@ -510,14 +520,19 @@ "watchify": "^3.7.0" }, "files": [ - "bin", - "images", - "lib", + "bin/{*mocha,options.js}", + "assets/growl/*.png", + "lib/**/*.{js,html}", "index.js", "mocha.css", "mocha.js", "browser-entry.js" ], + "browserify": { + "transform": [ + "package-json-versionify" + ] + }, "browser": { "growl": "./lib/browser/growl.js", "tty": "./lib/browser/tty.js", @@ -527,8 +542,6 @@ "path": false, "supports-color": false }, - "homepage": "https://mochajs.org", - "logo": "https://cldup.com/S9uQ-cOLYz.svg", "prettier": { "singleQuote": true, "bracketSpacing": false diff --git a/test/integration/deprecate.spec.js b/test/integration/deprecate.spec.js new file mode 100644 index 0000000000..db806fcde5 --- /dev/null +++ b/test/integration/deprecate.spec.js @@ -0,0 +1,18 @@ +'use strict'; + +var assert = require('assert'); +var run = require('./helpers').runMocha; +var args = []; + +describe('utils.deprecate test', function() { + it('should print unique deprecation only once', function(done) { + run('deprecate.fixture.js', args, function(err, res) { + if (err) { + return done(err); + } + var result = res.output.match(/deprecated thing/g) || []; + assert.equal(result.length, 2); + done(); + }); + }); +}); diff --git a/test/integration/fixtures/deprecate.fixture.js b/test/integration/fixtures/deprecate.fixture.js new file mode 100644 index 0000000000..cf98884600 --- /dev/null +++ b/test/integration/fixtures/deprecate.fixture.js @@ -0,0 +1,9 @@ +'use strict'; + +var utils = require("../../../lib/utils"); + +it('consolidates identical calls to deprecate', function() { + utils.deprecate("suite foo did a deprecated thing"); + utils.deprecate("suite foo did a deprecated thing"); + utils.deprecate("suite bar did a deprecated thing"); +}); diff --git a/test/integration/fixtures/reporters.fixture.js b/test/integration/fixtures/reporters.fixture.js new file mode 100644 index 0000000000..ab16bc0eed --- /dev/null +++ b/test/integration/fixtures/reporters.fixture.js @@ -0,0 +1,63 @@ +'use strict'; + +/** + * This file generates a wide range of output to test reporter functionality. + */ + +describe('Animals', function() { + + it('should consume organic material', function(done) { done(); }); + it('should breathe oxygen', function(done) { + // we're a jellyfish + var actualBreathe = 'nothing'; + var expectedBreathe = 'oxygen'; + expect(actualBreathe, 'to equal', expectedBreathe); + done(); + }); + it('should be able to move', function(done) { done(); }); + it('should reproduce sexually', function(done) { done(); }); + it('should grow from a hollow sphere of cells', function(done) { done(); }); + + describe('Vertebrates', function() { + describe('Mammals', function() { + it('should give birth to live young', function(done) { + var expectedMammal = { + consumesMaterial: 'organic', + breathe: 'oxygen', + reproduction: { + type: 'sexually', + spawnType: 'live', + } + }; + var platypus = JSON.parse(JSON.stringify(expectedMammal)); + platypus['reproduction']['spawnType'] = 'hard-shelled egg'; + + expect(platypus, 'to equal', expectedMammal); + done(); + }); + + describe('Blue Whale', function() { + it('should be the largest of all mammals', function(done) { done(); }); + it('should have a body in some shade of blue', function(done) { + var bodyColor = 'blueish_grey'; + var shadesOfBlue = ['cyan', 'light_blue', 'blue', 'indigo']; + expect(bodyColor, 'to be one of', shadesOfBlue); + + done(); + }); + }); + }); + describe('Birds', function() { + it('should have feathers', function(done) { done(); }); + it('should lay hard-shelled eggs', function(done) { done(); }); + }); + }); + + describe('Tardigrades', function() { + it('should answer to "water bear"', function(done) { done(); }); + it('should be able to survive global mass extinction events', function(done) { + throw new Error("How do we even test for this without causing one?") + done(); + }); + }); +}); diff --git a/test/integration/fixtures/suite/suite-returning-value.fixture.js b/test/integration/fixtures/suite/suite-returning-value.fixture.js new file mode 100644 index 0000000000..8859cfb5d9 --- /dev/null +++ b/test/integration/fixtures/suite/suite-returning-value.fixture.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('a suite returning a value', function () { + return Promise.resolve(); +}); diff --git a/test/integration/reporters.spec.js b/test/integration/reporters.spec.js index 4e95622dee..c78da8ab6b 100644 --- a/test/integration/reporters.spec.js +++ b/test/integration/reporters.spec.js @@ -99,4 +99,137 @@ describe('reporters', function() { }); }); }); + + describe('tap', function() { + var not = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + var versionPredicate = function(line) { + return line.match(/^TAP version \d+$/) != null; + }; + var planPredicate = function(line) { + return line.match(/^1\.\.\d+$/) != null; + }; + var testLinePredicate = function(line) { + return line.match(/^not ok/) != null || line.match(/^ok/) != null; + }; + var diagnosticPredicate = function(line) { + return line.match(/^#/) != null; + }; + var bailOutPredicate = function(line) { + return line.match(/^Bail out!/) != null; + }; + var anythingElsePredicate = function(line) { + return ( + versionPredicate(line) === false && + planPredicate(line) === false && + testLinePredicate(line) === false && + diagnosticPredicate(line) === false && + bailOutPredicate(line) === false + ); + }; + + describe('produces valid TAP v13 output', function() { + var runFixtureAndValidateOutput = function(fixture, expected) { + it('for ' + fixture, function(done) { + var args = ['--reporter=tap', '--reporter-options', 'tapVersion=13']; + + run(fixture, args, function(err, res) { + if (err) { + done(err); + return; + } + + var expectedVersion = 13; + var expectedPlan = '1..' + expected.numTests; + + var outputLines = res.output.split('\n'); + + // first line must be version line + expect( + outputLines[0], + 'to equal', + 'TAP version ' + expectedVersion + ); + + // plan must appear once + expect(outputLines, 'to contain', expectedPlan); + expect( + outputLines.filter(function(l) { + return l === expectedPlan; + }), + 'to have length', + 1 + ); + // plan cannot appear in middle of the output + var firstTestLine = outputLines.findIndex(testLinePredicate); + // there must be at least one test line + expect(firstTestLine, 'to be greater than', -1); + var lastTestLine = + outputLines.length - + 1 - + outputLines + .slice() + .reverse() + .findIndex(testLinePredicate); + var planLine = outputLines.findIndex(function(line) { + return line === expectedPlan; + }); + expect( + planLine < firstTestLine || planLine > lastTestLine, + 'to equal', + true + ); + + done(); + }); + }); + }; + + runFixtureAndValidateOutput('passing.fixture.js', { + numTests: 2 + }); + runFixtureAndValidateOutput('reporters.fixture.js', { + numTests: 12 + }); + }); + + it('places exceptions correctly in YAML blocks', function(done) { + var args = ['--reporter=tap', '--reporter-options', 'tapVersion=13']; + + run('reporters.fixture.js', args, function(err, res) { + if (err) { + done(err); + return; + } + + var outputLines = res.output.split('\n'); + + for (var i = 0; i + 1 < outputLines.length; i++) { + if ( + testLinePredicate(outputLines[i]) && + testLinePredicate(outputLines[i + 1]) === false + ) { + var blockLinesStart = i + 1; + var blockLinesEnd = + i + + 1 + + outputLines.slice(i + 1).findIndex(not(anythingElsePredicate)); + var blockLines = + blockLinesEnd > blockLinesStart + ? outputLines.slice(blockLinesStart, blockLinesEnd) + : outputLines.slice(blockLinesStart); + i += blockLines.length; + + expect(blockLines[0], 'to match', /^\s+---/); + expect(blockLines[blockLines.length - 1], 'to match', /^\s+\.\.\./); + } + } + + done(); + }); + }); + }); }); diff --git a/test/integration/suite.spec.js b/test/integration/suite.spec.js index e8bd34382b..338a198f63 100644 --- a/test/integration/suite.spec.js +++ b/test/integration/suite.spec.js @@ -9,8 +9,7 @@ describe('suite w/no callback', function() { it('should throw a helpful error message when a callback for suite is not supplied', function(done) { run('suite/suite-no-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var result = res.output.match(/no callback was supplied/) || []; assert.equal(result.length, 1); @@ -24,8 +23,7 @@ describe('skipped suite w/no callback', function() { it('should not throw an error when a callback for skipped suite is not supplied', function(done) { run('suite/suite-skipped-no-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var pattern = new RegExp('Error', 'g'); var result = res.output.match(pattern) || []; @@ -40,8 +38,7 @@ describe('skipped suite w/ callback', function() { it('should not throw an error when a callback for skipped suite is supplied', function(done) { run('suite/suite-skipped-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var pattern = new RegExp('Error', 'g'); var result = res.output.match(pattern) || []; @@ -50,3 +47,18 @@ describe('skipped suite w/ callback', function() { }); }); }); + +describe('suite returning a value', function() { + this.timeout(2000); + it('should give a deprecation warning for suite callback returning a value', function(done) { + run('suite/suite-returning-value.fixture.js', args, function(err, res) { + if (err) { + return done(err); + } + var pattern = new RegExp('Deprecation Warning', 'g'); + var result = res.output.match(pattern) || []; + assert.equal(result.length, 1); + done(); + }); + }); +}); diff --git a/test/reporters/base.spec.js b/test/reporters/base.spec.js index 8ff2195f70..f880b7b240 100644 --- a/test/reporters/base.spec.js +++ b/test/reporters/base.spec.js @@ -312,6 +312,7 @@ describe('Base reporter', function() { }); it('should interpret Chai custom error messages', function() { + this.timeout(1000); var chaiExpect = require('chai').expect; try { chaiExpect(43, 'custom error message').to.equal(42); diff --git a/test/reporters/doc.spec.js b/test/reporters/doc.spec.js index e3de653528..857922b83e 100644 --- a/test/reporters/doc.spec.js +++ b/test/reporters/doc.spec.js @@ -4,22 +4,15 @@ var reporters = require('../../').reporters; var Doc = reporters.Doc; var createMockRunner = require('./helpers.js').createMockRunner; +var makeRunReporter = require('./helpers.js').createRunReporterFunction; describe('Doc reporter', function() { - var stdout; - var stdoutWrite; var runner; - beforeEach(function() { - stdout = []; - stdoutWrite = process.stdout.write; - process.stdout.write = function(string, enc, callback) { - stdout.push(string); - stdoutWrite.call(process.stdout, string, enc, callback); - }; - }); + var options = {}; + var runReporter = makeRunReporter(Doc); afterEach(function() { - process.stdout.write = stdoutWrite; + runner = undefined; }); describe('on suite', function() { @@ -32,8 +25,7 @@ describe('Doc reporter', function() { }; it('should log html with indents and expected title', function() { runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
\n', '

' + expectedTitle + '

\n', @@ -48,8 +40,7 @@ describe('Doc reporter', function() { }; expectedTitle = '<div>' + expectedTitle + '</div>'; runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
\n', '

' + expectedTitle + '

\n', @@ -64,8 +55,7 @@ describe('Doc reporter', function() { }; it('should not log any html', function() { runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); expect(stdout, 'to be empty'); }); }); @@ -78,8 +68,7 @@ describe('Doc reporter', function() { }; it('should log expected html with indents', function() { runner = createMockRunner('suite end', 'suite end', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [' \n', '
\n']; expect(stdout, 'to equal', expectedArray); }); @@ -90,8 +79,7 @@ describe('Doc reporter', function() { }; it('should not log any html', function() { runner = createMockRunner('suite end', 'suite end', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); expect(stdout, 'to be empty'); }); }); @@ -109,8 +97,7 @@ describe('Doc reporter', function() { }; it('should log html with indents and expected title and body', function() { runner = createMockRunner('pass', 'pass', null, null, test); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', '
' + expectedBody + '
\n' @@ -128,8 +115,7 @@ describe('Doc reporter', function() { var expectedEscapedBody = '<div>' + expectedBody + '</div>'; runner = createMockRunner('pass', 'pass', null, null, test); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedEscapedTitle + '
\n', '
' + expectedEscapedBody + '
\n' @@ -158,8 +144,7 @@ describe('Doc reporter', function() { test, expectedError ); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', '
' +
@@ -190,8 +175,7 @@ describe('Doc reporter', function() {
         test,
         unescapedError
       );
-      Doc.call(this, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter(this, runner, options);
       var expectedArray = [
         '    
' + expectedEscapedTitle + '
\n', '
' +
diff --git a/test/reporters/dot.spec.js b/test/reporters/dot.spec.js
index 0940d8be5f..472ec63d01 100644
--- a/test/reporters/dot.spec.js
+++ b/test/reporters/dot.spec.js
@@ -5,42 +5,17 @@ var Dot = reporters.Dot;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers.js').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Dot reporter', function() {
-  var stdout;
   var runner;
   var useColors;
   var windowWidth;
   var color;
-  var showOutput = false;
-
-  /**
-   * Run reporter using stream reassignment to capture output.
-   *
-   * @param {Object} stubSelf - Reporter-like stub instance
-   * @param {Runner} runner - Mock instance
-   * @param {boolean} [tee=false] - If `true`, echo captured output to screen
-   */
-  function runReporter(stubSelf, runner, tee) {
-    // Reassign stream in order to make a copy of all reporter output
-    var stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      if (tee) {
-        stdoutWrite.call(process.stdout, string, enc, callback);
-      }
-    };
-
-    // Invoke reporter
-    Dot.call(stubSelf, runner);
-
-    // Revert stream reassignment here so reporter output
-    // can't be corrupted if any test assertions throw
-    process.stdout.write = stdoutWrite;
-  }
+  var options = {};
+  var runReporter = makeRunReporter(Dot);
 
   beforeEach(function() {
-    stdout = [];
     useColors = Base.useColors;
     windowWidth = Base.window.width;
     color = Base.color;
@@ -61,7 +36,7 @@ describe('Dot reporter', function() {
   describe('on start', function() {
     it('should write a newline', function() {
       runner = createMockRunner('start', 'start');
-      runReporter({epilogue: function() {}}, runner, showOutput);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -73,7 +48,7 @@ describe('Dot reporter', function() {
       });
       it('should write a newline followed by a comma', function() {
         runner = createMockRunner('pending', 'pending');
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['\n  ', 'pending_' + Base.symbols.comma];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -81,7 +56,7 @@ describe('Dot reporter', function() {
     describe('if window width is equal to or less than 1', function() {
       it('should write a comma', function() {
         runner = createMockRunner('pending', 'pending');
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['pending_' + Base.symbols.comma];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -101,7 +76,7 @@ describe('Dot reporter', function() {
       describe('if test speed is fast', function() {
         it('should write a newline followed by a dot', function() {
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'fast');
           var expectedArray = ['\n  ', 'fast_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -112,7 +87,7 @@ describe('Dot reporter', function() {
       describe('if test speed is fast', function() {
         it('should write a grey dot', function() {
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'fast');
           var expectedArray = ['fast_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -122,7 +97,7 @@ describe('Dot reporter', function() {
         it('should write a yellow dot', function() {
           test.duration = 2;
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'medium');
           var expectedArray = ['medium_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -132,7 +107,7 @@ describe('Dot reporter', function() {
         it('should write a bright yellow dot', function() {
           test.duration = 3;
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'slow');
           var expectedArray = ['bright-yellow_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -152,7 +127,7 @@ describe('Dot reporter', function() {
       });
       it('should write a newline followed by an exclamation mark', function() {
         runner = createMockRunner('fail', 'fail', null, null, test);
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['\n  ', 'fail_' + Base.symbols.bang];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -160,7 +135,7 @@ describe('Dot reporter', function() {
     describe('if window width is equal to or less than 1', function() {
       it('should write an exclamation mark', function() {
         runner = createMockRunner('fail', 'fail', null, null, test);
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['fail_' + Base.symbols.bang];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -173,7 +148,7 @@ describe('Dot reporter', function() {
       var epilogue = function() {
         epilogueCalled = true;
       };
-      runReporter({epilogue: epilogue}, runner, showOutput);
+      runReporter({epilogue: epilogue}, runner, options);
       expect(epilogueCalled, 'to be', true);
     });
   });
diff --git a/test/reporters/helpers.js b/test/reporters/helpers.js
index e0c6604216..4da36fa600 100644
--- a/test/reporters/helpers.js
+++ b/test/reporters/helpers.js
@@ -155,9 +155,51 @@ function makeExpectedTest(
   };
 }
 
+/**
+ * Creates closure with reference to the reporter class constructor.
+ *
+ * @param {Function} ctor - Reporter class constructor
+ * @return {createRunReporterFunction~runReporter}
+ */
+function createRunReporterFunction(ctor) {
+  /**
+   * Run reporter using stream reassignment to capture output.
+   *
+   * @param {Object} stubSelf - Reporter-like stub instance
+   * @param {Runner} runner - Mock instance
+   * @param {Object} [options] - Reporter configuration settings
+   * @param {boolean} [tee=false] - Whether to echo output to screen
+   * @return {string[]} Lines of output written to `stdout`
+   */
+  var runReporter = function(stubSelf, runner, options, tee) {
+    var stdout = [];
+
+    // Reassign stream in order to make a copy of all reporter output
+    var stdoutWrite = process.stdout.write;
+    process.stdout.write = function(string, enc, callback) {
+      stdout.push(string);
+      if (tee) {
+        stdoutWrite.call(process.stdout, string, enc, callback);
+      }
+    };
+
+    // Invoke reporter
+    ctor.call(stubSelf, runner, options);
+
+    // Revert stream reassignment here so reporter output
+    // can't be corrupted if any test assertions throw
+    process.stdout.write = stdoutWrite;
+
+    return stdout;
+  };
+
+  return runReporter;
+}
+
 module.exports = {
-  createMockRunner: createMockRunner,
-  makeTest: makeTest,
   createElements: createElements,
-  makeExpectedTest: makeExpectedTest
+  createMockRunner: createMockRunner,
+  createRunReporterFunction: createRunReporterFunction,
+  makeExpectedTest: makeExpectedTest,
+  makeTest: makeTest
 };
diff --git a/test/reporters/json-stream.spec.js b/test/reporters/json-stream.spec.js
index 3c5e1831f2..8c9fcfff35 100644
--- a/test/reporters/json-stream.spec.js
+++ b/test/reporters/json-stream.spec.js
@@ -5,12 +5,12 @@ var JSONStream = reporters.JSONStream;
 
 var createMockRunner = require('./helpers').createMockRunner;
 var makeExpectedTest = require('./helpers').makeExpectedTest;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
-describe('Json Stream reporter', function() {
+describe('JSON Stream reporter', function() {
   var runner;
-  var stdout;
-  var stdoutWrite;
-
+  var options = {};
+  var runReporter = makeRunReporter(JSONStream);
   var expectedTitle = 'some title';
   var expectedFullTitle = 'full title';
   var expectedDuration = 1000;
@@ -27,17 +27,8 @@ describe('Json Stream reporter', function() {
     message: expectedErrorMessage
   };
 
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
-
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
@@ -45,9 +36,7 @@ describe('Json Stream reporter', function() {
       runner = createMockRunner('start', 'start');
       var expectedTotal = 12;
       runner.total = expectedTotal;
-      JSONStream.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(
         stdout[0],
@@ -60,9 +49,7 @@ describe('Json Stream reporter', function() {
   describe('on pass', function() {
     it('should write stringified test data', function() {
       runner = createMockRunner('pass', 'pass', null, null, expectedTest);
-      JSONStream.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(
         stdout[0],
@@ -93,9 +80,7 @@ describe('Json Stream reporter', function() {
           expectedError
         );
 
-        JSONStream.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(
           stdout[0],
@@ -129,8 +114,7 @@ describe('Json Stream reporter', function() {
           expectedError
         );
 
-        JSONStream.call({}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter(this, runner, options);
 
         expect(
           stdout[0],
@@ -154,8 +138,7 @@ describe('Json Stream reporter', function() {
   describe('on end', function() {
     it('should write end details', function() {
       runner = createMockRunner('end', 'end');
-      JSONStream.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter(this, runner, options);
       expect(stdout[0], 'to match', /end/);
     });
   });
diff --git a/test/reporters/landing.spec.js b/test/reporters/landing.spec.js
index da4134ce70..1fee291e41 100644
--- a/test/reporters/landing.spec.js
+++ b/test/reporters/landing.spec.js
@@ -5,11 +5,12 @@ var Landing = reporters.Landing;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Landing reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Landing);
   var useColors;
   var windowWidth;
   var resetCode = '\u001b[0m';
@@ -25,12 +26,6 @@ describe('Landing reporter', function() {
   ];
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
     windowWidth = Base.window.width;
@@ -40,7 +35,7 @@ describe('Landing reporter', function() {
   afterEach(function() {
     Base.useColors = useColors;
     Base.window.width = windowWidth;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
@@ -48,9 +43,7 @@ describe('Landing reporter', function() {
       var cachedCursor = Base.cursor;
       Base.cursor.hide = function() {};
       runner = createMockRunner('start', 'start');
-      Landing.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(stdout[0], 'to equal', '\n\n\n  ');
       Base.cursor = cachedCursor;
@@ -63,9 +56,7 @@ describe('Landing reporter', function() {
         calledCursorHide = true;
       };
       runner = createMockRunner('start', 'start');
-      Landing.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({}, runner, options);
       expect(calledCursorHide, 'to be', true);
 
       Base.cursor = cachedCursor;
@@ -80,9 +71,7 @@ describe('Landing reporter', function() {
         };
         runner = createMockRunner('test end', 'test end', null, null, test);
         runner.total = 12;
-        Landing.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(stdout, 'to equal', expectedArray);
       });
@@ -94,9 +83,7 @@ describe('Landing reporter', function() {
         };
         runner = createMockRunner('test end', 'test end', null, null, test);
 
-        Landing.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(stdout, 'to equal', expectedArray);
       });
@@ -112,16 +99,16 @@ describe('Landing reporter', function() {
       runner = createMockRunner('end', 'end');
 
       var calledEpilogue = false;
-      Landing.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
 
-      process.stdout.write = stdoutWrite;
       expect(calledEpilogue, 'to be', true);
       expect(calledCursorShow, 'to be', true);
 
diff --git a/test/reporters/list.spec.js b/test/reporters/list.spec.js
index 7b9c74fb43..78895796c4 100644
--- a/test/reporters/list.spec.js
+++ b/test/reporters/list.spec.js
@@ -5,11 +5,12 @@ var List = reporters.List;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('List reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(List);
   var useColors;
   var expectedTitle = 'some title';
   var expectedDuration = 100;
@@ -21,27 +22,20 @@ describe('List reporter', function() {
     slow: function() {}
   };
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
   });
 
   afterEach(function() {
     Base.useColors = useColors;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start and test', function() {
     it('should write expected new line and title to the console', function() {
       runner = createMockRunner('start test', 'start', 'test', null, test);
-      List.call({epilogue: function() {}}, runner);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       var startString = '\n';
       var testString = '    ' + expectedTitle + ': ';
       var expectedArray = [startString, testString];
@@ -51,9 +45,7 @@ describe('List reporter', function() {
   describe('on pending', function() {
     it('should write expected title to the console', function() {
       runner = createMockRunner('pending test', 'pending', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(stdout[0], 'to equal', '  - ' + expectedTitle + '\n');
     });
@@ -66,9 +58,7 @@ describe('List reporter', function() {
         calledCursorCR = true;
       };
       runner = createMockRunner('pass', 'pass', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({epilogue: function() {}}, runner, options);
 
       expect(calledCursorCR, 'to be', true);
 
@@ -81,9 +71,7 @@ describe('List reporter', function() {
       var cachedCursor = Base.cursor;
       Base.cursor.CR = function() {};
       runner = createMockRunner('pass', 'pass', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(
         stdout[0],
@@ -109,9 +97,7 @@ describe('List reporter', function() {
         calledCursorCR = true;
       };
       runner = createMockRunner('fail', 'fail', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({epilogue: function() {}}, runner, options);
 
       expect(calledCursorCR, 'to be', true);
 
@@ -122,9 +108,7 @@ describe('List reporter', function() {
       var expectedErrorCount = 1;
       Base.cursor.CR = function() {};
       runner = createMockRunner('fail', 'fail', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(
         stdout[0],
@@ -140,6 +124,7 @@ describe('List reporter', function() {
       var checked = false;
       var err;
       test = {};
+      runner = createMockRunner('fail', 'fail', null, null, test);
       runner.on = runner.once = function(event, callback) {
         if (!checked && event === 'fail') {
           err = new Error('fake failure object with actual/expected');
@@ -150,9 +135,8 @@ describe('List reporter', function() {
           checked = true;
         }
       };
-      List.call({epilogue: function() {}}, runner);
+      runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       expect(typeof err.actual, 'to be', 'string');
       expect(typeof err.expected, 'to be', 'string');
     });
@@ -162,15 +146,15 @@ describe('List reporter', function() {
     it('should call epilogue', function() {
       var calledEpilogue = false;
       runner = createMockRunner('end', 'end');
-      List.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
-      process.stdout.write = stdoutWrite;
 
       expect(calledEpilogue, 'to be', true);
     });
diff --git a/test/reporters/markdown.spec.js b/test/reporters/markdown.spec.js
index f6fc63f119..842d7c7643 100644
--- a/test/reporters/markdown.spec.js
+++ b/test/reporters/markdown.spec.js
@@ -4,26 +4,18 @@ var reporters = require('../../').reporters;
 var Markdown = reporters.Markdown;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Markdown reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Markdown);
   var expectedTitle = 'expected title';
   var expectedFullTitle = 'full title';
   var sluggedFullTitle = 'full-title';
 
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
-
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe("on 'suite'", function() {
@@ -51,8 +43,7 @@ describe('Markdown reporter', function() {
         expectedSuite
       );
       runner.suite = expectedSuite;
-      Markdown.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedArray = [
         '# TOC\n',
@@ -97,8 +88,7 @@ describe('Markdown reporter', function() {
       };
       runner = createMockRunner('pass end', 'pass', 'end', null, expectedTest);
       runner.suite = expectedSuite;
-      Markdown.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedArray = [
         '# TOC\n',
diff --git a/test/reporters/min.spec.js b/test/reporters/min.spec.js
index 30b05126ec..f88adf31d7 100644
--- a/test/reporters/min.spec.js
+++ b/test/reporters/min.spec.js
@@ -4,31 +4,22 @@ var reporters = require('../../').reporters;
 var Min = reporters.Min;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Min reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
-
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
+  var options = {};
+  var runReporter = makeRunReporter(Min);
 
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
     it('should clear screen then set cursor position', function() {
       runner = createMockRunner('start', 'start');
-      Min.call({epilogue: function() {}}, runner);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[2J', '\u001b[1;3H'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -38,15 +29,15 @@ describe('Min reporter', function() {
     it('should call epilogue', function() {
       var calledEpilogue = false;
       runner = createMockRunner('end', 'end');
-      Min.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
-      process.stdout.write = stdoutWrite;
 
       expect(calledEpilogue, 'to be', true);
     });
diff --git a/test/reporters/nyan.spec.js b/test/reporters/nyan.spec.js
index 1296ab1195..96bbda6766 100644
--- a/test/reporters/nyan.spec.js
+++ b/test/reporters/nyan.spec.js
@@ -5,41 +5,33 @@ var NyanCat = reporters.Nyan;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Nyan reporter', function() {
   describe('events', function() {
     var runner;
-    var stdout;
-    var stdoutWrite;
     var calledDraw;
-
-    beforeEach(function() {
-      stdout = [];
-      stdoutWrite = process.stdout.write;
-      process.stdout.write = function(string, enc, callback) {
-        stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
-      };
-    });
+    var options = {};
+    var runReporter = makeRunReporter(NyanCat);
 
     afterEach(function() {
-      process.stdout.write = stdoutWrite;
+      runner = undefined;
     });
 
     describe('on start', function() {
       it('should call draw', function() {
         calledDraw = false;
         runner = createMockRunner('start', 'start');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -48,16 +40,16 @@ describe('Nyan reporter', function() {
       it('should call draw', function() {
         calledDraw = false;
         runner = createMockRunner('pending', 'pending');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -70,16 +62,16 @@ describe('Nyan reporter', function() {
           slow: function() {}
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -91,16 +83,16 @@ describe('Nyan reporter', function() {
           err: ''
         };
         runner = createMockRunner('fail', 'fail', null, null, test);
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -109,7 +101,7 @@ describe('Nyan reporter', function() {
       it('should call epilogue', function() {
         var calledEpilogue = false;
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {},
             generateColors: function() {},
@@ -117,28 +109,28 @@ describe('Nyan reporter', function() {
               calledEpilogue = true;
             }
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledEpilogue, 'to be', true);
       });
       it('should write numberOfLines amount of new lines', function() {
         var expectedNumberOfLines = 4;
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        var stdout = runReporter(
           {
             draw: function() {},
             generateColors: function() {},
             epilogue: function() {}
           },
-          runner
+          runner,
+          options
         );
 
         var arrayOfNewlines = stdout.filter(function(value) {
           return value === '\n';
         });
-        process.stdout.write = stdoutWrite;
 
         expect(arrayOfNewlines, 'to have length', expectedNumberOfLines);
       });
@@ -149,16 +141,16 @@ describe('Nyan reporter', function() {
           showCalled = true;
         };
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {},
             generateColors: function() {},
             epilogue: function() {}
           },
-          runner
+          runner,
+          options
         );
 
-        process.stdout.write = stdoutWrite;
         expect(showCalled, 'to be', true);
         Base.cursor.show = cachedShow;
       });
@@ -199,7 +191,6 @@ describe('Nyan reporter', function() {
           cursorUp: function() {}
         });
 
-        process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[0C',
           '_,------,',
@@ -235,7 +226,6 @@ describe('Nyan reporter', function() {
           cursorUp: function() {}
         });
 
-        // process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[0C',
           '_,------,',
@@ -276,7 +266,6 @@ describe('Nyan reporter', function() {
       var expectedNumber = 25;
 
       nyanCat.cursorDown(expectedNumber);
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[' + expectedNumber + 'B'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -303,7 +292,6 @@ describe('Nyan reporter', function() {
       var expectedNumber = 25;
 
       nyanCat.cursorUp(expectedNumber);
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[' + expectedNumber + 'A'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -432,13 +420,16 @@ describe('Nyan reporter', function() {
     var stdoutWrite;
     var stdout;
     var cachedColor;
+    var showOutput = false;
 
     beforeEach(function() {
       stdout = [];
       stdoutWrite = process.stdout.write;
       process.stdout.write = function(string, enc, callback) {
         stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
+        if (showOutput) {
+          stdoutWrite.call(process.stdout, string, enc, callback);
+        }
       };
       cachedColor = Base.color;
       Base.color = function(type, n) {
@@ -496,13 +487,16 @@ describe('Nyan reporter', function() {
   describe('drawRainbow', function() {
     var stdoutWrite;
     var stdout;
+    var showOutput = false;
 
     beforeEach(function() {
       stdout = [];
       stdoutWrite = process.stdout.write;
       process.stdout.write = function(string, enc, callback) {
         stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
+        if (showOutput) {
+          stdoutWrite.call(process.stdout, string, enc, callback);
+        }
       };
     });
 
@@ -524,7 +518,6 @@ describe('Nyan reporter', function() {
         numberOfLines: 1
       });
 
-      process.stdout.write = stdoutWrite;
       var expectedArray = [
         '\u001b[' + expectedWidth + 'C',
         expectedContents,
diff --git a/test/reporters/progress.spec.js b/test/reporters/progress.spec.js
index ef08ea5582..f19bb3988d 100644
--- a/test/reporters/progress.spec.js
+++ b/test/reporters/progress.spec.js
@@ -5,11 +5,13 @@ var Progress = reporters.Progress;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Progress reporter', function() {
   var stdout;
   var stdoutWrite;
   var runner;
+  var runReporter = makeRunReporter(Progress);
 
   beforeEach(function() {
     stdout = [];
@@ -32,9 +34,8 @@ describe('Progress reporter', function() {
         calledCursorHide = true;
       };
       runner = createMockRunner('start', 'start');
-      Progress.call({}, runner);
+      runReporter({}, runner, {});
 
-      process.stdout.write = stdoutWrite;
       expect(calledCursorHide, 'to be', true);
 
       Base.cursor = cachedCursor;
@@ -55,9 +56,7 @@ describe('Progress reporter', function() {
         var expectedOptions = {};
         runner = createMockRunner('test end', 'test end');
         runner.total = expectedTotal;
-        Progress.call({}, runner, expectedOptions);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, expectedOptions);
 
         expect(stdout, 'to equal', []);
 
@@ -93,9 +92,8 @@ describe('Progress reporter', function() {
         };
         runner = createMockRunner('test end', 'test end');
         runner.total = expectedTotal;
-        Progress.call({}, runner, options);
+        var stdout = runReporter({}, runner, options);
 
-        process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[J',
           '  ' + expectedOpen,
@@ -122,16 +120,16 @@ describe('Progress reporter', function() {
       };
       runner = createMockRunner('end', 'end');
       var calledEpilogue = false;
-      Progress.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        {}
       );
 
-      process.stdout.write = stdoutWrite;
       expect(calledEpilogue, 'to be', true);
       expect(calledCursorShow, 'to be', true);
 
diff --git a/test/reporters/spec.spec.js b/test/reporters/spec.spec.js
index 2f4eee33e5..5f7584e33e 100644
--- a/test/reporters/spec.spec.js
+++ b/test/reporters/spec.spec.js
@@ -5,28 +5,23 @@ var Spec = reporters.Spec;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Spec reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Spec);
   var useColors;
   var expectedTitle = 'expectedTitle';
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
   });
 
   afterEach(function() {
     Base.useColors = useColors;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on suite', function() {
@@ -35,8 +30,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('suite', 'suite', null, null, suite);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = [expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -47,8 +41,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('pending test', 'pending', null, null, suite);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['  - ' + expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -65,8 +58,7 @@ describe('Spec reporter', function() {
           }
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        Spec.call({epilogue: function() {}}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedString =
           '  ' +
           Base.symbols.ok +
@@ -90,8 +82,7 @@ describe('Spec reporter', function() {
           }
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        Spec.call({epilogue: function() {}}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedString =
           '  ' + Base.symbols.ok + ' ' + expectedTitle + '\n';
         expect(stdout[0], 'to be', expectedString);
@@ -105,8 +96,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('fail', 'fail', null, null, test);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['  ' + functionCount + ') ' + expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
diff --git a/test/reporters/tap.spec.js b/test/reporters/tap.spec.js
index 5030cbde5c..c0a5419d37 100644
--- a/test/reporters/tap.spec.js
+++ b/test/reporters/tap.spec.js
@@ -4,22 +4,16 @@ var reporters = require('../../').reporters;
 var TAP = reporters.TAP;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('TAP reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var runReporter = makeRunReporter(TAP);
   var expectedTitle = 'some title';
   var countAfterTestEnd = 2;
   var test;
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     test = {
       fullTitle: function() {
         return expectedTitle;
@@ -29,213 +23,456 @@ describe('TAP reporter', function() {
   });
 
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
+    test = undefined;
   });
 
-  describe('on start', function() {
-    it('should hand runners suite into grepTotal and log the total', function() {
+  describe('TAP12 spec', function() {
+    var options = {};
+
+    describe('on start', function() {
       var expectedSuite = 'some suite';
       var expectedTotal = 10;
       var expectedString;
-      runner = createMockRunner('start', 'start');
-      runner.suite = expectedSuite;
-      runner.grepTotal = function(string) {
-        expectedString = string;
-        return expectedTotal;
-      };
-      TAP.call({}, runner);
-
-      var expectedArray = ['1..' + expectedTotal + '\n'];
-      process.stdout.write = stdoutWrite;
-
-      expect(stdout, 'to equal', expectedArray);
-      expect(expectedString, 'to be', expectedSuite);
+      var stdout;
+
+      before(function() {
+        runner = createMockRunner('start', 'start');
+        runner.suite = expectedSuite;
+        runner.grepTotal = function(string) {
+          expectedString = string;
+          return expectedTotal;
+        };
+        stdout = runReporter({}, runner, options);
+      });
+
+      it('should not write the TAP specification version', function() {
+        expect(stdout, 'not to contain', 'TAP version');
+      });
+      it('should write the number of tests that it plans to run', function() {
+        var expectedArray = ['1..' + expectedTotal + '\n'];
+        expect(stdout, 'to equal', expectedArray);
+        expect(expectedString, 'to be', expectedSuite);
+      });
     });
-  });
 
-  describe('on pending', function() {
-    it('should write expected message including count and title', function() {
-      runner = createMockRunner(
-        'start test',
-        'test end',
-        'pending',
-        null,
-        test
-      );
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      TAP.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
-
-      var expectedMessage =
-        'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
-      expect(stdout[0], 'to equal', expectedMessage);
+    describe('on pending', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner(
+          'start test',
+          'test end',
+          'pending',
+          null,
+          test
+        );
+        runner.suite = '';
+        runner.grepTotal = function() {};
+
+        var stdout = runReporter({}, runner, options);
+
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
+        expect(stdout[0], 'to equal', expectedMessage);
+      });
     });
-  });
 
-  describe('on pass', function() {
-    it('should write expected message including count and title', function() {
-      runner = createMockRunner('start test', 'test end', 'pass', null, test);
+    describe('on pass', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner('start test', 'test end', 'pass', null, test);
+        runner.suite = '';
+        runner.grepTotal = function() {};
 
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      TAP.call({}, runner);
+        var stdout = runReporter({}, runner, options);
 
-      process.stdout.write = stdoutWrite;
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
+        expect(stdout[0], 'to equal', expectedMessage);
+      });
+    });
+
+    describe('on fail', function() {
+      describe('if there is an error message', function() {
+        it('should write expected message and error message', function() {
+          var expectedErrorMessage = 'some error';
+          var error = {
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedErrorMessage + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var error = {
+            stack: expectedStack
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedStack + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack and error message', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var expectedErrorMessage = 'some error';
+          var error = {
+            stack: expectedStack,
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
 
-      var expectedMessage =
-        'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
-      expect(stdout[0], 'to equal', expectedMessage);
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedErrorMessage + '\n',
+            '  ' + expectedStack + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is no error stack or error message', function() {
+        it('should write expected message only', function() {
+          var error = {};
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = runner.once = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
     });
-  });
 
-  describe('on fail', function() {
-    describe('if there is an error message', function() {
-      it('should write expected message and error message', function() {
-        var expectedTitle = 'some title';
-        var countAfterTestEnd = 2;
-        var expectedErrorMessage = 'some error';
-        var test = {
-          fullTitle: function() {
-            return expectedTitle;
-          },
-          slow: function() {}
-        };
-        var error = {
-          message: expectedErrorMessage
-        };
-        runner.on = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+    describe('on end', function() {
+      it('should write total tests, passes and failures', function() {
+        var numberOfPasses = 1;
+        var numberOfFails = 1;
+        runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
 
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
+        var totalTests = numberOfPasses + numberOfFails;
         var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedErrorMessage + '\n'
+          'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
+          'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
+          '# tests ' + totalTests + '\n',
+          '# pass ' + numberOfPasses + '\n',
+          '# fail ' + numberOfFails + '\n'
         ];
         expect(stdout, 'to equal', expectedArray);
       });
     });
-    describe('if there is an error stack', function() {
-      it('should write expected message and stack', function() {
-        var expectedStack = 'some stack';
-        var error = {
-          stack: expectedStack
+  });
+
+  describe('TAP13 spec', function() {
+    var options = {
+      reporterOptions: {
+        tapVersion: '13'
+      }
+    };
+
+    describe('on start', function() {
+      var expectedSuite = 'some suite';
+      var expectedTotal = 10;
+      var expectedString;
+      var stdout;
+
+      before(function() {
+        runner = createMockRunner('start', 'start');
+        runner.suite = expectedSuite;
+        runner.grepTotal = function(string) {
+          expectedString = string;
+          return expectedTotal;
         };
+
+        stdout = runReporter({}, runner, options);
+      });
+
+      it('should write the TAP specification version', function() {
+        var tapVersion = options.reporterOptions.tapVersion;
+        var expectedFirstLine = 'TAP version ' + tapVersion + '\n';
+        expect(stdout[0], 'to equal', expectedFirstLine);
+      });
+      it('should write the number of tests that it plans to run', function() {
+        var expectedSecondLine = '1..' + expectedTotal + '\n';
+        expect(stdout[1], 'to equal', expectedSecondLine);
+        expect(expectedString, 'to be', expectedSuite);
+      });
+    });
+
+    describe('on pending', function() {
+      it('should write expected message including count and title', function() {
         runner = createMockRunner(
-          'test end fail',
+          'start test',
           'test end',
-          'fail',
+          'pending',
           null,
-          test,
-          error
+          test
         );
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
 
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
-        var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedStack + '\n'
-        ];
-        expect(stdout, 'to equal', expectedArray);
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
+        expect(stdout[0], 'to equal', expectedMessage);
       });
     });
-    describe('if there is an error stack and error message', function() {
-      it('should write expected message and stack', function() {
-        var expectedTitle = 'some title';
-        var countAfterTestEnd = 2;
-        var expectedStack = 'some stack';
-        var expectedErrorMessage = 'some error';
-        var test = {
-          fullTitle: function() {
-            return expectedTitle;
-          },
-          slow: function() {}
-        };
-        var error = {
-          stack: expectedStack,
-          message: expectedErrorMessage
-        };
-        runner.on = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+
+    describe('on pass', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner('start test', 'test end', 'pass', null, test);
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
 
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
-        var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedErrorMessage + '\n',
-          '  ' + expectedStack + '\n'
-        ];
-        expect(stdout, 'to equal', expectedArray);
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
+        expect(stdout[0], 'to equal', expectedMessage);
       });
     });
-    describe('if there is no error stack or error message', function() {
-      it('should write expected message only', function() {
-        var error = {};
-        runner.on = runner.once = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+
+    describe('on fail', function() {
+      describe('if there is an error message', function() {
+        it('should write expected message and error message', function() {
+          var expectedErrorMessage = 'some error';
+          var error = {
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    message: |-\n',
+            '      ' + expectedErrorMessage + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var error = {
+            stack: expectedStack
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    stack: |-\n',
+            '      ' + expectedStack + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack and error message', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var expectedErrorMessage = 'some error';
+          var error = {
+            stack: expectedStack,
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    message: |-\n',
+            '      ' + expectedErrorMessage + '\n',
+            '    stack: |-\n',
+            '      ' + expectedStack + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is no error stack or error message', function() {
+        it('should write expected message only', function() {
+          var error = {};
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = runner.once = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+    });
+
+    describe('on end', function() {
+      it('should write total tests, passes and failures', function() {
+        var numberOfPasses = 1;
+        var numberOfFails = 1;
+        runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
 
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
+        var totalTests = numberOfPasses + numberOfFails;
         var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
+          'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
+          '# tests ' + totalTests + '\n',
+          '# pass ' + numberOfPasses + '\n',
+          '# fail ' + numberOfFails + '\n'
         ];
         expect(stdout, 'to equal', expectedArray);
       });
     });
   });
-
-  describe('on end', function() {
-    it('should write total tests, passes and failures', function() {
-      var numberOfPasses = 1;
-      var numberOfFails = 1;
-      runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      TAP.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
-
-      var totalTests = numberOfPasses + numberOfFails;
-      var expectedArray = [
-        'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
-        'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
-        '# tests ' + totalTests + '\n',
-        '# pass ' + numberOfPasses + '\n',
-        '# fail ' + numberOfFails + '\n'
-      ];
-      expect(stdout, 'to equal', expectedArray);
-    });
-  });
 });
diff --git a/test/reporters/xunit.spec.js b/test/reporters/xunit.spec.js
index 8108c50b78..42e9ca2c6e 100644
--- a/test/reporters/xunit.spec.js
+++ b/test/reporters/xunit.spec.js
@@ -18,6 +18,8 @@ describe('XUnit reporter', function() {
   var expectedClassName = 'fullTitle';
   var expectedTitle = 'some title';
   var expectedMessage = 'some message';
+  var expectedDiff =
+    '\n      + expected - actual\n\n      -foo\n      +bar\n      ';
   var expectedStack = 'some-stack';
   var expectedWrite = null;
 
@@ -214,6 +216,8 @@ describe('XUnit reporter', function() {
           },
           duration: 1000,
           err: {
+            actual: 'foo',
+            expected: 'bar',
             message: expectedMessage,
             stack: expectedStack
           }
@@ -235,6 +239,8 @@ describe('XUnit reporter', function() {
           '" time="1">' +
           expectedMessage +
           '\n' +
+          expectedDiff +
+          '\n' +
           expectedStack +
           '';
 
diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js
index 4bd654915f..13e5a6db02 100644
--- a/test/unit/mocha.spec.js
+++ b/test/unit/mocha.spec.js
@@ -34,6 +34,16 @@ describe('Mocha', function() {
         });
       }
     );
+
+    it('should emit start event', function(done) {
+      var mocha = new Mocha(blankOpts);
+      mocha.run().on('start', done);
+    });
+
+    it('should emit end event', function(done) {
+      var mocha = new Mocha(blankOpts);
+      mocha.run().on('end', done);
+    });
   });
 
   describe('.addFile()', function() {
diff --git a/test/unit/runnable.spec.js b/test/unit/runnable.spec.js
index 9ea00b6d07..0fb29db6e5 100644
--- a/test/unit/runnable.spec.js
+++ b/test/unit/runnable.spec.js
@@ -46,18 +46,84 @@ describe('Runnable(title, fn)', function() {
   });
 
   describe('#timeout(ms)', function() {
-    it('should set the timeout', function() {
-      var run = new Runnable();
-      run.timeout(1000);
-      assert(run.timeout() === 1000);
+    var MIN_TIMEOUT = 0;
+    var MAX_TIMEOUT = 2147483647; // INT_MAX (32-bit signed integer)
+
+    describe('when value is less than lower bound', function() {
+      it('should clamp to lower bound given numeric', function() {
+        var run = new Runnable();
+        run.timeout(-1);
+        assert(run.timeout() === MIN_TIMEOUT);
+      });
+      // :TODO: Our internal version of `ms` can't handle negative time,
+      // but package version can. Skip this check until that change is merged.
+      it.skip('should clamp to lower bound given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('-1 ms');
+        assert(run.timeout() === MIN_TIMEOUT);
+      });
     });
-  });
 
-  describe('#timeout(ms) when ms>2^31', function() {
-    it('should set disabled', function() {
-      var run = new Runnable();
-      run.timeout(1e10);
-      assert(run.enableTimeouts() === false);
+    describe('when value is equal to lower bound', function() {
+      it('should set the value and disable timeouts given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MIN_TIMEOUT);
+        assert(run.timeout() === MIN_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should set the value and disable timeouts given timestamp', function() {
+        var run = new Runnable();
+        run.timeout(MIN_TIMEOUT + 'ms');
+        assert(run.timeout() === MIN_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+    });
+
+    describe('when value is within `setTimeout` bounds', function() {
+      var oneSecond = 1000;
+
+      it('should set the timeout given numeric', function() {
+        var run = new Runnable();
+        run.timeout(oneSecond);
+        assert(run.timeout() === oneSecond);
+        assert(run.enableTimeouts() === true);
+      });
+      it('should set the timeout given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('1s');
+        assert(run.timeout() === oneSecond);
+        assert(run.enableTimeouts() === true);
+      });
+    });
+
+    describe('when value is equal to upper bound', function() {
+      it('should set the value and disable timeout given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT);
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should set the value and disable timeout given timestamp', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT + 'ms');
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+    });
+
+    describe('when value is greater than `setTimeout` limit', function() {
+      it('should clamp to upper bound given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT + 1);
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should clamp to upper bound given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('24.9d'); // 2151360000ms
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
     });
   });