diff --git a/docs/index.md b/docs/index.md index d8a76a8a8d..0c8bb08e6f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -73,6 +73,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js](https://n - [Configuring Mocha (Node.js)](#configuring-mocha-nodejs) - [`mocha.opts`](#mochaopts) - [The `test/` Directory](#the-test-directory) +- [Error Codes](#error-codes) - [Editor Plugins](#editor-plugins) - [Examples](#examples) - [Testing Mocha](#testing-mocha) @@ -1658,6 +1659,20 @@ $ mocha "./spec/**/*.js" *Note*: Double quotes around the glob are recommended for portability. +## Error Codes + +List of codes associated with Errors thrown inside Mocha. Following NodeJS practices. + +| Code | Meaning | +| ------------- | ------------- | +| ERR_MOCHA_INVALID_ARG_TYPE | argument of the wrong type was passed to Mocha's API | +| ERR_MOCHA_INVALID_ARG_VALUE | invalid or unsupported value was passed for a given argument | +| ERR_MOCHA_INVALID_INTERFACE | interface specified in options not found | +| ERR_MOCHA_INVALID_REPORTER | reporter specified in options not found | +| ERR_MOCHA_NO_FILES_MATCH_PATTERN | file/s of test could not be found | +| ERR_MOCHA_NOT_SUPPORTED | type of output specified was not supported | +| ERR_MOCHA_UNDEFINED_ERROR | an error was thrown but no details were specified | + ## Editor Plugins The following editor-related packages are available: diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index 3afa7143d3..5212d386db 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -143,10 +143,8 @@ exports.handleFiles = ({ try { newFiles = utils.lookupFiles(arg, extension, recursive); } catch (err) { - if (err.message.indexOf('cannot resolve path') === 0) { - console.error( - `Warning: Could not find any test files matching pattern: ${arg}` - ); + if (err.code === 'ERR_MOCHA_NO_FILES_MATCH_PATTERN') { + console.warn('Warning: %s: %O', err.message, err.pattern); return; } diff --git a/lib/cli/run.js b/lib/cli/run.js index 1d6bf1ac4c..e6c14f67ad 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -9,6 +9,8 @@ const Mocha = require('../mocha'); const ansi = require('ansi-colors'); +const errors = require('../errors'); +const createInvalidArgumentValueError = errors.createInvalidArgumentValueError; const { list, @@ -190,7 +192,12 @@ exports.builder = yargs => const pair = opt.split('='); if (pair.length > 2 || !pair.length) { - throw new Error(`invalid reporter option '${opt}'`); + throw createInvalidArgumentValueError( + `invalid reporter option '${opt}'`, + '--reporter-option', + opt, + 'expected "key=value" format' + ); } acc[pair[0]] = pair.length === 2 ? pair[1] : true; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000000..90d0b92d05 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,139 @@ +'use strict'; +/** + * @module Errors + */ +/** + * Factory functions to create throwable error objects + */ + +/** + * Creates an error object used when no files to be tested could be found using specified pattern. + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} pattern - User-specified argument value. + * @returns {Error} instance detailing the error condition + */ +function createNoFilesMatchPatternError(message, pattern) { + var err = new Error(message); + err.code = 'ERR_MOCHA_NO_FILES_MATCH_PATTERN'; + err.pattern = pattern; + return err; +} + +/** + * Creates an error object used when the reporter specified in the options was not found. + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} reporter - User-specified reporter value. + * @returns {Error} instance detailing the error condition + */ +function createInvalidReporterError(message, reporter) { + var err = new TypeError(message); + err.code = 'ERR_MOCHA_INVALID_REPORTER'; + err.reporter = reporter; + return err; +} + +/** + * Creates an error object used when the interface specified in the options was not found. + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} ui - User-specified interface value. + * @returns {Error} instance detailing the error condition + */ +function createInvalidInterfaceError(message, ui) { + var err = new Error(message); + err.code = 'ERR_MOCHA_INVALID_INTERFACE'; + err.interface = ui; + return err; +} + +/** + * Creates an error object used when the type of output specified was not supported. + * + * @public + * @param {string} message - Error message to be displayed. + * @returns {Error} instance detailing the error condition + */ +function createNotSupportedError(message) { + var err = new Error(message); + err.code = 'ERR_MOCHA_NOT_SUPPORTED'; + return err; +} + +/** + * Creates an error object used when an argument is missing. + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} argument - Argument name. + * @param {string} expected - Expected argument datatype. + * @returns {Error} instance detailing the error condition + */ +function createMissingArgumentError(message, argument, expected) { + return createInvalidArgumentTypeError(message, argument, expected); +} + +/** + * Creates an error object used when an argument did not use the supported type + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} argument - Argument name. + * @param {string} expected - Expected argument datatype. + * @returns {Error} instance detailing the error condition + */ +function createInvalidArgumentTypeError(message, argument, expected) { + var err = new TypeError(message); + err.code = 'ERR_MOCHA_INVALID_ARG_TYPE'; + err.argument = argument; + err.expected = expected; + err.actual = typeof argument; + return err; +} + +/** + * Creates an error object used when an argument did not use the supported value + * + * @public + * @param {string} message - Error message to be displayed. + * @param {string} argument - Argument name. + * @param {string} value - Argument value. + * @param {string} [reason] - Why value is invalid. + * @returns {Error} instance detailing the error condition + */ +function createInvalidArgumentValueError(message, argument, value, reason) { + var err = new TypeError(message); + err.code = 'ERR_MOCHA_INVALID_ARG_VALUE'; + err.argument = argument; + err.value = value; + err.reason = typeof reason !== 'undefined' ? reason : 'is invalid'; + return err; +} + +/** + * Creates an error object used when an error was thrown but no details were specified. + * + * @public + * @param {string} message - Error message to be displayed. + * @returns {Error} instance detailing the error condition + */ +function createUndefinedError(message) { + var err = new Error(message); + err.code = 'ERR_MOCHA_UNDEFINED_ERROR'; + return err; +} + +module.exports = { + createInvalidArgumentTypeError: createInvalidArgumentTypeError, + createInvalidArgumentValueError: createInvalidArgumentValueError, + createInvalidInterfaceError: createInvalidInterfaceError, + createInvalidReporterError: createInvalidReporterError, + createMissingArgumentError: createMissingArgumentError, + createNoFilesMatchPatternError: createNoFilesMatchPatternError, + createNotSupportedError: createNotSupportedError, + createUndefinedError: createUndefinedError +}; diff --git a/lib/interfaces/common.js b/lib/interfaces/common.js index 3c6dffa96f..1656ce2aaa 100644 --- a/lib/interfaces/common.js +++ b/lib/interfaces/common.js @@ -2,6 +2,8 @@ var Suite = require('../suite'); var utils = require('../utils'); +var errors = require('../errors'); +var createMissingArgumentError = errors.createMissingArgumentError; /** * Functions common to more than one interface. @@ -147,11 +149,13 @@ module.exports = function(suites, context, mocha) { } suites.shift(); } else if (typeof opts.fn === 'undefined' && !suite.pending) { - throw new Error( + throw createMissingArgumentError( 'Suite "' + suite.fullTitle() + '" was defined but no callback was supplied. ' + - 'Supply a callback or explicitly skip the suite.' + 'Supply a callback or explicitly skip the suite.', + 'callback', + 'function' ); } else if (!opts.fn && suite.pending) { suites.shift(); diff --git a/lib/mocha.js b/lib/mocha.js index cce2bb9b2f..c3d9f99b96 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -12,6 +12,9 @@ var builtinReporters = require('./reporters'); var utils = require('./utils'); var mocharc = require('./mocharc.json'); var assign = require('object.assign').getPolyfill(); +var errors = require('./errors'); +var createInvalidReporterError = errors.createInvalidReporterError; +var createInvalidInterfaceError = errors.createInvalidInterfaceError; exports = module.exports = Mocha; @@ -220,12 +223,16 @@ Mocha.prototype.reporter = function(reporter, reporterOptions) { try { _reporter = require(reporter); } catch (err) { - if (err.message.indexOf('Cannot find module') !== -1) { + if ( + err.code !== 'MODULE_NOT_FOUND' || + err.message.includes('Cannot find module') + ) { // Try to load reporters from a path (absolute or relative) try { _reporter = require(path.resolve(process.cwd(), reporter)); } catch (_err) { - err.message.indexOf('Cannot find module') !== -1 + _err.code !== 'MODULE_NOT_FOUND' || + _err.message.includes('Cannot find module') ? console.warn('"' + reporter + '" reporter not found') : console.warn( '"' + @@ -249,7 +256,10 @@ Mocha.prototype.reporter = function(reporter, reporterOptions) { ); } if (!_reporter) { - throw new Error('invalid reporter "' + reporter + '"'); + throw createInvalidReporterError( + 'invalid reporter "' + reporter + '"', + reporter + ); } this._reporter = _reporter; } @@ -275,7 +285,10 @@ Mocha.prototype.ui = function(name) { try { this._ui = require(name); } catch (err) { - throw new Error('invalid interface "' + name + '"'); + throw createInvalidInterfaceError( + 'invalid interface "' + name + '"', + name + ); } } this._ui = this._ui(this.suite); diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index f559207b18..643367aa9d 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -13,7 +13,8 @@ var fs = require('fs'); var escape = utils.escape; var mkdirp = require('mkdirp'); var path = require('path'); - +var errors = require('../errors'); +var createNotSupportedError = errors.createNotSupportedError; /** * Save timer references to avoid Sinon interfering (see GH-237). */ @@ -50,7 +51,7 @@ function XUnit(runner, options) { if (options && options.reporterOptions) { if (options.reporterOptions.output) { if (!fs.createWriteStream) { - throw new Error('file output not supported in browser'); + throw createNotSupportedError('file output not supported in browser'); } mkdirp.sync(path.dirname(options.reporterOptions.output)); diff --git a/lib/suite.js b/lib/suite.js index 482a47925b..64e7db4b24 100644 --- a/lib/suite.js +++ b/lib/suite.js @@ -12,6 +12,8 @@ var utils = require('./utils'); var inherits = utils.inherits; var debug = require('debug')('mocha:suite'); var milliseconds = require('ms'); +var errors = require('./errors'); +var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError; /** * Expose `Suite`. @@ -49,10 +51,12 @@ exports.create = function(parent, title) { */ function Suite(title, parentContext) { if (!utils.isString(title)) { - throw new Error( - 'Suite `title` should be a "string" but "' + + throw createInvalidArgumentTypeError( + 'Suite argument "title" must be a string. Received type "' + typeof title + - '" was given instead.' + '"', + 'title', + 'string' ); } this.title = title; diff --git a/lib/test.js b/lib/test.js index d8233a3571..6c6aeb6b7a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,6 +1,8 @@ 'use strict'; var Runnable = require('./runnable'); var utils = require('./utils'); +var errors = require('./errors'); +var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError; var isString = utils.isString; module.exports = Test; @@ -15,10 +17,12 @@ module.exports = Test; */ function Test(title, fn) { if (!isString(title)) { - throw new Error( - 'Test `title` should be a "string" but "' + + throw createInvalidArgumentTypeError( + 'Test argument "title" should be a string. Received type "' + typeof title + - '" was given instead.' + '"', + 'title', + 'string' ); } Runnable.call(this, title, fn); diff --git a/lib/utils.js b/lib/utils.js index 71aa7804e8..d89b35ccdb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,6 +14,10 @@ var glob = require('glob'); var path = require('path'); var join = path.join; var he = require('he'); +var errors = require('./errors'); +var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError; +var createMissingArgumentError = errors.createMissingArgumentError; +var createUndefinedError = errors.createUndefinedError; /** * Ignored directories. @@ -515,7 +519,10 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { } else { files = glob.sync(filepath); if (!files.length) { - throw new Error("cannot resolve path (or pattern) '" + filepath + "'"); + throw createNoFilesMatchPatternError( + 'cannot find any files matching pattern "' + filepath + '"', + filepath + ); } return files; } @@ -546,8 +553,10 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { return; } if (!extensions) { - throw new Error( - 'extensions parameter required when filepath is a directory' + throw createMissingArgumentError( + 'Argument "extensions" required when argument "filepath" is a directory', + 'extensions', + 'array' ); } var re = new RegExp('\\.(?:' + extensions.join('|') + ')$'); @@ -567,7 +576,7 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { */ exports.undefinedError = function() { - return new Error( + return createUndefinedError( 'Caught undefined error, did you throw without specifying what?' ); }; diff --git a/test/integration/file-utils.spec.js b/test/integration/file-utils.spec.js index 74c30be46e..3fe030ee46 100644 --- a/test/integration/file-utils.spec.js +++ b/test/integration/file-utils.spec.js @@ -75,11 +75,11 @@ describe('file utils', function() { var dirLookup = function() { return utils.lookupFiles(tmpDir, undefined, false); }; - expect( - dirLookup, - 'to throw', - 'extensions parameter required when filepath is a directory' - ); + expect(dirLookup, 'to throw', { + name: 'TypeError', + code: 'ERR_MOCHA_INVALID_ARG_TYPE', + argument: 'extensions' + }); }); }); diff --git a/test/integration/glob.spec.js b/test/integration/glob.spec.js index 29d7ec64aa..6aa7563bf5 100644 --- a/test/integration/glob.spec.js +++ b/test/integration/glob.spec.js @@ -28,7 +28,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern "./*-none.js"' ); }, done @@ -47,7 +47,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern' ); }, done @@ -77,7 +77,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern' ); }, done @@ -96,7 +96,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern' ); }, done @@ -125,7 +125,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern' ); }, done @@ -144,7 +144,7 @@ describe('globbing', function() { expect( results.stderr, 'to contain', - 'Could not find any test files matching pattern' + 'cannot find any files matching pattern' ); }, done diff --git a/test/integration/suite.spec.js b/test/integration/suite.spec.js index 93450c9582..cd379dc329 100644 --- a/test/integration/suite.spec.js +++ b/test/integration/suite.spec.js @@ -1,6 +1,5 @@ 'use strict'; -var assert = require('assert'); var run = require('./helpers').runMocha; var args = []; @@ -13,8 +12,9 @@ describe('suite w/no callback', function() { if (err) { return done(err); } - var result = res.output.match(/no callback was supplied/) || []; - assert.strictEqual(result.length, 1); + var pattern = new RegExp('TypeError', 'g'); + var result = res.output.match(pattern) || []; + expect(result, 'to have length', 2); done(); }, {stdio: 'pipe'} @@ -28,9 +28,9 @@ describe('skipped suite w/no callback', function() { if (err) { return done(err); } - var pattern = new RegExp('Error', 'g'); + var pattern = new RegExp('TypeError', 'g'); var result = res.output.match(pattern) || []; - assert.strictEqual(result.length, 0); + expect(result, 'to have length', 0); done(); }); }); @@ -42,9 +42,9 @@ describe('skipped suite w/ callback', function() { if (err) { return done(err); } - var pattern = new RegExp('Error', 'g'); + var pattern = new RegExp('TypeError', 'g'); var result = res.output.match(pattern) || []; - assert.strictEqual(result.length, 0); + expect(result, 'to have length', 0); done(); }); }); @@ -61,7 +61,7 @@ describe('suite returning a value', function() { } var pattern = new RegExp('Deprecation Warning', 'g'); var result = res.output.match(pattern) || []; - assert.strictEqual(result.length, 1); + expect(result, 'to have length', 1); done(); }, {stdio: 'pipe'} diff --git a/test/reporters/helpers.js b/test/reporters/helpers.js index bd49392a40..a507f25c81 100644 --- a/test/reporters/helpers.js +++ b/test/reporters/helpers.js @@ -1,5 +1,7 @@ 'use strict'; +var errors = require('../../lib/errors'); +var createNotSupportedError = errors.createNotSupportedError; /* This function prevents the constant use of creating a runnerEvent. runStr is the argument that defines the runnerEvent. @@ -116,7 +118,7 @@ function createRunnerFunction(runStr, ifStr1, ifStr2, ifStr3, arg1, arg2) { } }; default: - throw new Error( + throw createNotSupportedError( 'This function does not support the runner string specified.' ); } diff --git a/test/unit/errors.spec.js b/test/unit/errors.spec.js new file mode 100644 index 0000000000..51d066ef98 --- /dev/null +++ b/test/unit/errors.spec.js @@ -0,0 +1,28 @@ +'use strict'; + +var errors = require('../../lib/errors'); + +describe('Errors', function() { + var expectedMessage = 'some message'; + it('should include expected code in thrown reporter errors', function() { + var throwError = function() { + throw errors.createInvalidReporterError(expectedMessage, 'badReporter'); + }; + expect(throwError, 'to throw', { + message: expectedMessage, + code: 'ERR_MOCHA_INVALID_REPORTER', + reporter: 'badReporter' + }); + }); + + it('should include expected code in thrown interface errors', function() { + var throwError = function() { + throw errors.createInvalidInterfaceError(expectedMessage, 'badUi'); + }; + expect(throwError, 'to throw', { + message: expectedMessage, + code: 'ERR_MOCHA_INVALID_INTERFACE', + interface: 'badUi' + }); + }); +}); diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index 2511b1204d..46c20513f8 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -197,4 +197,19 @@ describe('Mocha', function() { expect(mocha.suite._bail, 'to be', true); }); }); + + describe('error handling', function() { + it('should throw reporter error if an invalid reporter is given', function() { + var updatedOpts = {reporter: 'invalidReporter', reporterOptions: {}}; + var throwError = function() { + // eslint-disable-next-line no-new + new Mocha(updatedOpts); + }; + expect(throwError, 'to throw', { + message: 'invalid reporter "invalidReporter"', + code: 'ERR_MOCHA_INVALID_REPORTER', + reporter: 'invalidReporter' + }); + }); + }); });