mocha 7.1.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +143 -2
  2. package/bin/mocha +24 -4
  3. package/browser-entry.js +37 -9
  4. package/lib/browser/growl.js +2 -1
  5. package/lib/browser/highlight-tags.js +39 -0
  6. package/lib/browser/parse-query.js +24 -0
  7. package/lib/browser/progress.js +4 -0
  8. package/lib/browser/template.html +7 -5
  9. package/lib/cli/cli.js +6 -3
  10. package/lib/cli/collect-files.js +17 -10
  11. package/lib/cli/config.js +6 -6
  12. package/lib/cli/init.js +1 -2
  13. package/lib/cli/lookup-files.js +145 -0
  14. package/lib/cli/node-flags.js +2 -2
  15. package/lib/cli/options.js +14 -90
  16. package/lib/cli/run-helpers.js +133 -36
  17. package/lib/cli/run-option-metadata.js +4 -2
  18. package/lib/cli/run.js +71 -11
  19. package/lib/cli/watch-run.js +211 -51
  20. package/lib/context.js +0 -15
  21. package/lib/errors.js +202 -9
  22. package/lib/esm-utils.js +11 -6
  23. package/lib/hook.js +32 -0
  24. package/lib/interfaces/common.js +21 -10
  25. package/lib/mocha.js +301 -126
  26. package/lib/mocharc.json +0 -1
  27. package/lib/nodejs/buffered-worker-pool.js +174 -0
  28. package/lib/{growl.js → nodejs/growl.js} +3 -2
  29. package/lib/nodejs/parallel-buffered-runner.js +295 -0
  30. package/lib/nodejs/reporters/parallel-buffered.js +133 -0
  31. package/lib/nodejs/serializer.js +404 -0
  32. package/lib/nodejs/worker.js +154 -0
  33. package/lib/pending.js +4 -0
  34. package/lib/reporters/base.js +25 -12
  35. package/lib/reporters/doc.js +6 -0
  36. package/lib/reporters/json-stream.js +1 -0
  37. package/lib/reporters/json.js +1 -0
  38. package/lib/reporters/landing.js +11 -3
  39. package/lib/reporters/tap.js +1 -2
  40. package/lib/reporters/xunit.js +3 -2
  41. package/lib/runnable.js +39 -47
  42. package/lib/runner.js +219 -118
  43. package/lib/suite.js +61 -27
  44. package/lib/test.js +48 -3
  45. package/lib/utils.js +33 -209
  46. package/mocha.js +25522 -17715
  47. package/mocha.js.map +1 -0
  48. package/package.json +87 -68
  49. package/lib/browser/tty.js +0 -13
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var glob = require('glob');
6
+ var {format} = require('util');
7
+ var errors = require('../errors');
8
+ var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError;
9
+ var createMissingArgumentError = errors.createMissingArgumentError;
10
+ var {sQuote, dQuote} = require('../utils');
11
+
12
+ /**
13
+ * Determines if pathname would be a "hidden" file (or directory) on UN*X.
14
+ *
15
+ * @description
16
+ * On UN*X, pathnames beginning with a full stop (aka dot) are hidden during
17
+ * typical usage. Dotfiles, plain-text configuration files, are prime examples.
18
+ *
19
+ * @see {@link http://xahlee.info/UnixResource_dir/writ/unix_origin_of_dot_filename.html|Origin of Dot File Names}
20
+ *
21
+ * @private
22
+ * @param {string} pathname - Pathname to check for match.
23
+ * @return {boolean} whether pathname would be considered a hidden file.
24
+ * @example
25
+ * isHiddenOnUnix('.profile'); // => true
26
+ */
27
+ function isHiddenOnUnix(pathname) {
28
+ return path.basename(pathname)[0] === '.';
29
+ }
30
+
31
+ /**
32
+ * Determines if pathname has a matching file extension.
33
+ *
34
+ * @private
35
+ * @param {string} pathname - Pathname to check for match.
36
+ * @param {string[]} exts - List of file extensions (sans period).
37
+ * @return {boolean} whether file extension matches.
38
+ * @example
39
+ * hasMatchingExtname('foo.html', ['js', 'css']); // => false
40
+ */
41
+ function hasMatchingExtname(pathname, exts) {
42
+ var suffix = path.extname(pathname).slice(1);
43
+ return exts.some(function(element) {
44
+ return suffix === element;
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Lookup file names at the given `path`.
50
+ *
51
+ * @description
52
+ * Filenames are returned in _traversal_ order by the OS/filesystem.
53
+ * **Make no assumption that the names will be sorted in any fashion.**
54
+ *
55
+ * @public
56
+ * @memberof Mocha.utils
57
+ * @param {string} filepath - Base path to start searching from.
58
+ * @param {string[]} [extensions=[]] - File extensions to look for.
59
+ * @param {boolean} [recursive=false] - Whether to recurse into subdirectories.
60
+ * @return {string[]} An array of paths.
61
+ * @throws {Error} if no files match pattern.
62
+ * @throws {TypeError} if `filepath` is directory and `extensions` not provided.
63
+ */
64
+ module.exports = function lookupFiles(filepath, extensions, recursive) {
65
+ extensions = extensions || [];
66
+ recursive = recursive || false;
67
+ var files = [];
68
+ var stat;
69
+
70
+ if (!fs.existsSync(filepath)) {
71
+ var pattern;
72
+ if (glob.hasMagic(filepath)) {
73
+ // Handle glob as is without extensions
74
+ pattern = filepath;
75
+ } else {
76
+ // glob pattern e.g. 'filepath+(.js|.ts)'
77
+ var strExtensions = extensions
78
+ .map(function(v) {
79
+ return '.' + v;
80
+ })
81
+ .join('|');
82
+ pattern = filepath + '+(' + strExtensions + ')';
83
+ }
84
+ files = glob.sync(pattern, {nodir: true});
85
+ if (!files.length) {
86
+ throw createNoFilesMatchPatternError(
87
+ 'Cannot find any files matching pattern ' + dQuote(filepath),
88
+ filepath
89
+ );
90
+ }
91
+ return files;
92
+ }
93
+
94
+ // Handle file
95
+ try {
96
+ stat = fs.statSync(filepath);
97
+ if (stat.isFile()) {
98
+ return filepath;
99
+ }
100
+ } catch (err) {
101
+ // ignore error
102
+ return;
103
+ }
104
+
105
+ // Handle directory
106
+ fs.readdirSync(filepath).forEach(function(dirent) {
107
+ var pathname = path.join(filepath, dirent);
108
+ var stat;
109
+
110
+ try {
111
+ stat = fs.statSync(pathname);
112
+ if (stat.isDirectory()) {
113
+ if (recursive) {
114
+ files = files.concat(lookupFiles(pathname, extensions, recursive));
115
+ }
116
+ return;
117
+ }
118
+ } catch (err) {
119
+ // ignore error
120
+ return;
121
+ }
122
+ if (!extensions.length) {
123
+ throw createMissingArgumentError(
124
+ format(
125
+ 'Argument %s required when argument %s is a directory',
126
+ sQuote('extensions'),
127
+ sQuote('filepath')
128
+ ),
129
+ 'extensions',
130
+ 'array'
131
+ );
132
+ }
133
+
134
+ if (
135
+ !stat.isFile() ||
136
+ !hasMatchingExtname(pathname, extensions) ||
137
+ isHiddenOnUnix(pathname)
138
+ ) {
139
+ return;
140
+ }
141
+ files.push(pathname);
142
+ });
143
+
144
+ return files;
145
+ };
@@ -6,7 +6,7 @@
6
6
  * @module
7
7
  */
8
8
 
9
- const nodeFlags = require('node-environment-flags');
9
+ const nodeFlags = process.allowedNodeEnvironmentFlags;
10
10
  const unparse = require('yargs-unparser');
11
11
 
12
12
  /**
@@ -48,7 +48,7 @@ exports.isNodeFlag = (flag, bareword = true) => {
48
48
  // check actual node flags from `process.allowedNodeEnvironmentFlags`,
49
49
  // then historical support for various V8 and non-`NODE_OPTIONS` flags
50
50
  // and also any V8 flags with `--v8-` prefix
51
- (nodeFlags.has(flag) ||
51
+ ((nodeFlags && nodeFlags.has(flag)) ||
52
52
  debugFlags.has(flag) ||
53
53
  /(?:preserve-symlinks(?:-main)?|harmony(?:[_-]|$)|(?:trace[_-].+$)|gc(?:[_-]global)?$|es[_-]staging$|use[_-]strict$|v8[_-](?!options).+?$)/.test(
54
54
  flag
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Main entry point for handling filesystem-based configuration,
5
- * whether that's `mocha.opts` or a config file or `package.json` or whatever.
5
+ * whether that's a config file or `package.json` or whatever.
6
6
  * @module
7
7
  */
8
8
 
@@ -15,7 +15,6 @@ const mocharc = require('../mocharc.json');
15
15
  const {list} = require('./run-helpers');
16
16
  const {loadConfig, findConfig} = require('./config');
17
17
  const findUp = require('find-up');
18
- const {deprecate} = require('../utils');
19
18
  const debug = require('debug')('mocha:cli:options');
20
19
  const {isNodeFlag} = require('./node-flags');
21
20
 
@@ -55,16 +54,19 @@ const configuration = Object.assign({}, YARGS_PARSER_CONFIG, {
55
54
 
56
55
  /**
57
56
  * This is a really fancy way to:
58
- * - ensure unique values for `array`-type options
59
- * - use its array's last element for `boolean`/`number`/`string`- options given multiple times
57
+ * - `array`-type options: ensure unique values and evtl. split comma-delimited lists
58
+ * - `boolean`/`number`/`string`- options: use last element when given multiple times
60
59
  * This is passed as the `coerce` option to `yargs-parser`
61
60
  * @private
62
61
  * @ignore
63
62
  */
63
+ const globOptions = ['spec', 'ignore'];
64
64
  const coerceOpts = Object.assign(
65
65
  types.array.reduce(
66
66
  (acc, arg) =>
67
- Object.assign(acc, {[arg]: v => Array.from(new Set(list(v)))}),
67
+ Object.assign(acc, {
68
+ [arg]: v => Array.from(new Set(globOptions.includes(arg) ? v : list(v)))
69
+ }),
68
70
  {}
69
71
  ),
70
72
  types.boolean
@@ -143,71 +145,6 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => {
143
145
  return result.argv;
144
146
  };
145
147
 
146
- /**
147
- * - Replaces comments with empty strings
148
- * - Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space
149
- * - Splits on whitespace, creating array of substrings
150
- * - Filters empty string elements from array
151
- * - Replaces any HTML space with space
152
- * @summary Parses options read from run-control file.
153
- * @private
154
- * @param {string} content - Content read from run-control file.
155
- * @returns {string[]} cmdline options (and associated arguments)
156
- * @ignore
157
- */
158
- const parseMochaOpts = content =>
159
- content
160
- .replace(/^#.*$/gm, '')
161
- .replace(/\\\s/g, '%20')
162
- .split(/\s/)
163
- .filter(Boolean)
164
- .map(value => value.replace(/%20/g, ' '));
165
-
166
- /**
167
- * Given filepath in `args.opts`, attempt to load and parse a `mocha.opts` file.
168
- * @param {Object} [args] - Arguments object
169
- * @param {string|boolean} [args.opts] - Filepath to mocha.opts; defaults to whatever's in `mocharc.opts`, or `false` to skip
170
- * @returns {external:yargsParser.Arguments|void} If read, object containing parsed arguments
171
- * @memberof module:lib/cli/options
172
- * @see {@link /#mochaopts|mocha.opts}
173
- * @public
174
- */
175
- const loadMochaOpts = (args = {}) => {
176
- let result;
177
- let filepath = args.opts;
178
- // /dev/null is backwards compat
179
- if (filepath === false || filepath === '/dev/null') {
180
- return result;
181
- }
182
- filepath = filepath || mocharc.opts;
183
- result = {};
184
- let mochaOpts;
185
- try {
186
- mochaOpts = fs.readFileSync(filepath, 'utf8');
187
- debug(`read ${filepath}`);
188
- } catch (err) {
189
- if (args.opts) {
190
- throw new Error(`Unable to read ${filepath}: ${err}`);
191
- }
192
- // ignore otherwise. we tried
193
- debug(`No mocha.opts found at ${filepath}`);
194
- }
195
-
196
- // real args should override `mocha.opts` which should override defaults.
197
- // if there's an exception to catch here, I'm not sure what it is.
198
- // by attaching the `no-opts` arg, we avoid re-parsing of `mocha.opts`.
199
- if (mochaOpts) {
200
- deprecate(
201
- 'Configuration via mocha.opts is DEPRECATED and will be removed from a future version of Mocha. Use RC files or package.json instead.'
202
- );
203
- result = parse(parseMochaOpts(mochaOpts));
204
- debug(`${filepath} parsed succesfully`);
205
- }
206
- return result;
207
- };
208
-
209
- module.exports.loadMochaOpts = loadMochaOpts;
210
-
211
148
  /**
212
149
  * Given path to config file in `args.config`, attempt to load & parse config file.
213
150
  * @param {Object} [args] - Arguments object
@@ -244,16 +181,16 @@ const loadPkgRc = (args = {}) => {
244
181
  try {
245
182
  const pkg = JSON.parse(fs.readFileSync(filepath, 'utf8'));
246
183
  if (pkg.mocha) {
247
- debug(`'mocha' prop of package.json parsed:`, pkg.mocha);
184
+ debug('`mocha` prop of package.json parsed: %O', pkg.mocha);
248
185
  result = pkg.mocha;
249
186
  } else {
250
- debug(`no config found in ${filepath}`);
187
+ debug('no config found in %s', filepath);
251
188
  }
252
189
  } catch (err) {
253
190
  if (args.package) {
254
191
  throw new Error(`Unable to read/parse ${filepath}: ${err}`);
255
192
  }
256
- debug(`failed to read default package.json at ${filepath}; ignoring`);
193
+ debug('failed to read default package.json at %s; ignoring', filepath);
257
194
  }
258
195
  }
259
196
  return result;
@@ -267,11 +204,10 @@ module.exports.loadPkgRc = loadPkgRc;
267
204
  * 1. Command-line args
268
205
  * 2. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
269
206
  * 3. `mocha` prop of `package.json`
270
- * 4. `mocha.opts`
271
- * 5. default configuration (`lib/mocharc.json`)
207
+ * 4. default configuration (`lib/mocharc.json`)
272
208
  *
273
209
  * If a {@link module:lib/cli/one-and-dones.ONE_AND_DONE_ARGS "one-and-done" option} is present in the `argv` array, no external config files will be read.
274
- * @summary Parses options read from `mocha.opts`, `.mocharc.*` and `package.json`.
210
+ * @summary Parses options read from `.mocharc.*` and `package.json`.
275
211
  * @param {string|string[]} [argv] - Arguments to parse
276
212
  * @public
277
213
  * @memberof module:lib/cli/options
@@ -279,7 +215,7 @@ module.exports.loadPkgRc = loadPkgRc;
279
215
  */
280
216
  const loadOptions = (argv = []) => {
281
217
  let args = parse(argv);
282
- // short-circuit: look for a flag that would abort loading of mocha.opts
218
+ // short-circuit: look for a flag that would abort loading of options
283
219
  if (
284
220
  Array.from(ONE_AND_DONE_ARGS).reduce(
285
221
  (acc, arg) => acc || arg in args,
@@ -291,7 +227,6 @@ const loadOptions = (argv = []) => {
291
227
 
292
228
  const rcConfig = loadRc(args);
293
229
  const pkgConfig = loadPkgRc(args);
294
- const optsConfig = loadMochaOpts(args);
295
230
 
296
231
  if (rcConfig) {
297
232
  args.config = false;
@@ -301,19 +236,8 @@ const loadOptions = (argv = []) => {
301
236
  args.package = false;
302
237
  args._ = args._.concat(pkgConfig._ || []);
303
238
  }
304
- if (optsConfig) {
305
- args.opts = false;
306
- args._ = args._.concat(optsConfig._ || []);
307
- }
308
239
 
309
- args = parse(
310
- args._,
311
- mocharc,
312
- args,
313
- rcConfig || {},
314
- pkgConfig || {},
315
- optsConfig || {}
316
- );
240
+ args = parse(args._, mocharc, args, rcConfig || {}, pkgConfig || {});
317
241
 
318
242
  // recombine positional arguments and "spec"
319
243
  if (args.spec) {
@@ -10,10 +10,12 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const debug = require('debug')('mocha:cli:run:helpers');
13
- const watchRun = require('./watch-run');
13
+ const {watchRun, watchParallelRun} = require('./watch-run');
14
14
  const collectFiles = require('./collect-files');
15
-
16
- const cwd = (exports.cwd = process.cwd());
15
+ const {type} = require('../utils');
16
+ const {format} = require('util');
17
+ const {createInvalidPluginError, createUnsupportedError} = require('../errors');
18
+ const {requireOrImport} = require('../esm-utils');
17
19
 
18
20
  /**
19
21
  * Exits Mocha when tests + code under test has finished execution (default)
@@ -73,20 +75,66 @@ exports.list = str =>
73
75
  Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);
74
76
 
75
77
  /**
76
- * `require()` the modules as required by `--require <require>`
78
+ * `require()` the modules as required by `--require <require>`.
79
+ *
80
+ * Returns array of `mochaHooks` exports, if any.
77
81
  * @param {string[]} requires - Modules to require
82
+ * @returns {Promise<MochaRootHookObject|MochaRootHookFunction>} Any root hooks
78
83
  * @private
79
84
  */
80
- exports.handleRequires = (requires = []) => {
81
- requires.forEach(mod => {
85
+ exports.handleRequires = async (requires = []) => {
86
+ const acc = [];
87
+ for (const mod of requires) {
82
88
  let modpath = mod;
83
- if (fs.existsSync(mod, {cwd}) || fs.existsSync(`${mod}.js`, {cwd})) {
89
+ // this is relative to cwd
90
+ if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
84
91
  modpath = path.resolve(mod);
85
- debug(`resolved ${mod} to ${modpath}`);
92
+ debug('resolved required file %s to %s', mod, modpath);
86
93
  }
87
- require(modpath);
88
- debug(`loaded require "${mod}"`);
89
- });
94
+ const requiredModule = await requireOrImport(modpath);
95
+ if (
96
+ requiredModule &&
97
+ typeof requiredModule === 'object' &&
98
+ requiredModule.mochaHooks
99
+ ) {
100
+ const mochaHooksType = type(requiredModule.mochaHooks);
101
+ if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') {
102
+ debug('found root hooks in required file %s', mod);
103
+ acc.push(requiredModule.mochaHooks);
104
+ } else {
105
+ throw createUnsupportedError(
106
+ 'mochaHooks must be an object or a function returning (or fulfilling with) an object'
107
+ );
108
+ }
109
+ }
110
+ debug('loaded required module "%s"', mod);
111
+ }
112
+ return acc;
113
+ };
114
+
115
+ /**
116
+ * Loads root hooks as exported via `mochaHooks` from required files.
117
+ * These can be sync/async functions returning objects, or just objects.
118
+ * Flattens to a single object.
119
+ * @param {Array<MochaRootHookObject|MochaRootHookFunction>} rootHooks - Array of root hooks
120
+ * @private
121
+ * @returns {MochaRootHookObject}
122
+ */
123
+ exports.loadRootHooks = async rootHooks => {
124
+ const rootHookObjects = await Promise.all(
125
+ rootHooks.map(async hook => (/function$/.test(type(hook)) ? hook() : hook))
126
+ );
127
+
128
+ return rootHookObjects.reduce(
129
+ (acc, hook) => {
130
+ acc.beforeAll = acc.beforeAll.concat(hook.beforeAll || []);
131
+ acc.beforeEach = acc.beforeEach.concat(hook.beforeEach || []);
132
+ acc.afterAll = acc.afterAll.concat(hook.afterAll || []);
133
+ acc.afterEach = acc.afterEach.concat(hook.afterEach || []);
134
+ return acc;
135
+ },
136
+ {beforeAll: [], beforeEach: [], afterAll: [], afterEach: []}
137
+ );
90
138
  };
91
139
 
92
140
  /**
@@ -101,32 +149,61 @@ exports.handleRequires = (requires = []) => {
101
149
  */
102
150
  const singleRun = async (mocha, {exit}, fileCollectParams) => {
103
151
  const files = collectFiles(fileCollectParams);
104
- debug('running tests with files', files);
152
+ debug('single run with %d file(s)', files.length);
105
153
  mocha.files = files;
106
154
 
155
+ // handles ESM modules
107
156
  await mocha.loadFilesAsync();
108
157
  return mocha.run(exit ? exitMocha : exitMochaLater);
109
158
  };
110
159
 
111
160
  /**
112
- * Actually run tests
161
+ * Collect files and run tests (using `BufferedRunner`).
162
+ *
163
+ * This is `async` for consistency.
164
+ *
113
165
  * @param {Mocha} mocha - Mocha instance
114
- * @param {Object} opts - Command line options
166
+ * @param {Options} options - Command line options
167
+ * @param {Object} fileCollectParams - Parameters that control test
168
+ * file collection. See `lib/cli/collect-files.js`.
169
+ * @returns {Promise<BufferedRunner>}
170
+ * @ignore
115
171
  * @private
116
- * @returns {Promise}
172
+ */
173
+ const parallelRun = async (mocha, options, fileCollectParams) => {
174
+ const files = collectFiles(fileCollectParams);
175
+ debug(
176
+ 'executing %d test file(s) across %d concurrent jobs',
177
+ files.length,
178
+ options.jobs
179
+ );
180
+ mocha.files = files;
181
+
182
+ // note that we DO NOT load any files here; this is handled by the worker
183
+ return mocha.run(options.exit ? exitMocha : exitMochaLater);
184
+ };
185
+
186
+ /**
187
+ * Actually run tests. Delegates to one of four different functions:
188
+ * - `singleRun`: run tests in serial & exit
189
+ * - `watchRun`: run tests in serial, rerunning as files change
190
+ * - `parallelRun`: run tests in parallel & exit
191
+ * - `watchParallelRun`: run tests in parallel, rerunning as files change
192
+ * @param {Mocha} mocha - Mocha instance
193
+ * @param {Options} opts - Command line options
194
+ * @private
195
+ * @returns {Promise<Runner>}
117
196
  */
118
197
  exports.runMocha = async (mocha, options) => {
119
198
  const {
120
199
  watch = false,
121
200
  extension = [],
122
- exit = false,
123
201
  ignore = [],
124
202
  file = [],
203
+ parallel = false,
125
204
  recursive = false,
126
205
  sort = false,
127
- spec = [],
128
- watchFiles,
129
- watchIgnore
206
+ spec = []
130
207
  } = options;
131
208
 
132
209
  const fileCollectParams = {
@@ -138,43 +215,63 @@ exports.runMocha = async (mocha, options) => {
138
215
  spec
139
216
  };
140
217
 
218
+ let run;
141
219
  if (watch) {
142
- watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
220
+ run = parallel ? watchParallelRun : watchRun;
143
221
  } else {
144
- await singleRun(mocha, {exit}, fileCollectParams);
222
+ run = parallel ? parallelRun : singleRun;
145
223
  }
224
+
225
+ return run(mocha, options, fileCollectParams);
146
226
  };
147
227
 
148
228
  /**
149
- * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts
150
- * that it actually exists.
151
- * @todo XXX This must get run after requires are processed, as it'll prevent
152
- * interfaces from loading.
229
+ * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts that
230
+ * it actually exists. This must be run _after_ requires are processed (see
231
+ * {@link handleRequires}), as it'll prevent interfaces from loading otherwise.
153
232
  * @param {Object} opts - Options object
154
- * @param {string} key - Resolvable module name or path
155
- * @param {Object} [map] - An object perhaps having key `key`
233
+ * @param {"reporter"|"interface"} pluginType - Type of plugin.
234
+ * @param {Object} [map] - An object perhaps having key `key`. Used as a cache
235
+ * of sorts; `Mocha.reporters` is one, where each key corresponds to a reporter
236
+ * name
156
237
  * @private
157
238
  */
158
- exports.validatePlugin = (opts, key, map = {}) => {
159
- if (Array.isArray(opts[key])) {
160
- throw new TypeError(`"--${key} <${key}>" can only be specified once`);
239
+ exports.validatePlugin = (opts, pluginType, map = {}) => {
240
+ /**
241
+ * This should be a unique identifier; either a string (present in `map`),
242
+ * or a resolvable (via `require.resolve`) module ID/path.
243
+ * @type {string}
244
+ */
245
+ const pluginId = opts[pluginType];
246
+
247
+ if (Array.isArray(pluginId)) {
248
+ throw createInvalidPluginError(
249
+ `"--${pluginType}" can only be specified once`,
250
+ pluginType
251
+ );
161
252
  }
162
253
 
163
- const unknownError = () => new Error(`Unknown "${key}": ${opts[key]}`);
254
+ const unknownError = err =>
255
+ createInvalidPluginError(
256
+ format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err),
257
+ pluginType,
258
+ pluginId
259
+ );
164
260
 
165
- if (!map[opts[key]]) {
261
+ // if this exists, then it's already loaded, so nothing more to do.
262
+ if (!map[pluginId]) {
166
263
  try {
167
- opts[key] = require(opts[key]);
264
+ opts[pluginType] = require(pluginId);
168
265
  } catch (err) {
169
266
  if (err.code === 'MODULE_NOT_FOUND') {
170
267
  // Try to load reporters from a path (absolute or relative)
171
268
  try {
172
- opts[key] = require(path.resolve(process.cwd(), opts[key]));
269
+ opts[pluginType] = require(path.resolve(pluginId));
173
270
  } catch (err) {
174
- throw unknownError();
271
+ throw unknownError(err);
175
272
  }
176
273
  } else {
177
- throw unknownError();
274
+ throw unknownError(err);
178
275
  }
179
276
  }
180
277
  }
@@ -42,16 +42,16 @@ exports.types = {
42
42
  'list-interfaces',
43
43
  'list-reporters',
44
44
  'no-colors',
45
+ 'parallel',
45
46
  'recursive',
46
47
  'sort',
47
48
  'watch'
48
49
  ],
49
- number: ['retries'],
50
+ number: ['retries', 'jobs'],
50
51
  string: [
51
52
  'config',
52
53
  'fgrep',
53
54
  'grep',
54
- 'opts',
55
55
  'package',
56
56
  'reporter',
57
57
  'ui',
@@ -76,7 +76,9 @@ exports.aliases = {
76
76
  growl: ['G'],
77
77
  ignore: ['exclude'],
78
78
  invert: ['i'],
79
+ jobs: ['j'],
79
80
  'no-colors': ['C'],
81
+ parallel: ['p'],
80
82
  reporter: ['R'],
81
83
  'reporter-option': ['reporter-options', 'O'],
82
84
  require: ['r'],