mocha 9.0.0 → 9.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ # 9.1.0 / 2021-08-20
2
+
3
+ ## :tada: Enhancements
4
+
5
+ - [#4716](https://github.com/mochajs/mocha/issues/4716): Add new option `--fail-zero` ([**@juergba**](https://github.com/juergba))
6
+ - [#4691](https://github.com/mochajs/mocha/issues/4691): Add new option `--node-option` ([**@juergba**](https://github.com/juergba))
7
+ - [#4607](https://github.com/mochajs/mocha/issues/4607): Add output option to `JSON` reporter ([**@dorny**](https://github.com/dorny))
8
+
9
+ # 9.0.3 / 2021-07-25
10
+
11
+ ## :bug: Fixes
12
+
13
+ - [#4702](https://github.com/mochajs/mocha/issues/4702): Error rethrow from cwd-relative path while loading `.mocharc.js` ([**@kirill-golovan**](https://github.com/kirill-golovan))
14
+
15
+ - [#4688](https://github.com/mochajs/mocha/issues/4688): Usage of custom interface in parallel mode ([**@juergba**](https://github.com/juergba))
16
+
17
+ - [#4687](https://github.com/mochajs/mocha/issues/4687): ESM: don't swallow `MODULE_NOT_FOUND` errors in case of `type:module` ([**@giltayar**](https://github.com/giltayar))
18
+
19
+ # 9.0.2 / 2021-07-03
20
+
21
+ ## :bug: Fixes
22
+
23
+ - [#4668](https://github.com/mochajs/mocha/issues/4668): ESM: make `--require <dir>` work with new `import`-first loading ([**@giltayar**](https://github.com/giltayar))
24
+
25
+ ## :nut_and_bolt: Other
26
+
27
+ - [#4674](https://github.com/mochajs/mocha/issues/4674): Update production dependencies ([**@juergba**](https://github.com/juergba))
28
+
29
+ # 9.0.1 / 2021-06-18
30
+
31
+ ## :nut_and_bolt: Other
32
+
33
+ - [#4657](https://github.com/mochajs/mocha/issues/4657): Browser: add separate bundle for modern browsers ([**@juergba**](https://github.com/juergba))
34
+
35
+ We added a separate browser bundle `mocha-es2018.js` in javascript ES2018, as we skipped the transpilation down to ES5. This is an **experimental step towards freezing Mocha's support of IE11**.
36
+
37
+ - [#4653](https://github.com/mochajs/mocha/issues/4653): ESM: proper version check in `hasStableEsmImplementation` ([**@alexander-fenster**](https://github.com/alexander-fenster))
38
+
1
39
  # 9.0.0 / 2021-06-07
2
40
 
3
41
  ## :boom: Breaking Changes
package/bin/mocha CHANGED
@@ -10,7 +10,6 @@
10
10
  * @private
11
11
  */
12
12
 
13
- const {deprecate} = require('../lib/utils');
14
13
  const {loadOptions} = require('../lib/cli/options');
15
14
  const {
16
15
  unparseNodeFlags,
@@ -23,6 +22,7 @@ const {aliases} = require('../lib/cli/run-option-metadata');
23
22
 
24
23
  const mochaArgs = {};
25
24
  const nodeArgs = {};
25
+ let hasInspect = false;
26
26
 
27
27
  const opts = loadOptions(process.argv.slice(2));
28
28
  debug('loaded opts', opts);
@@ -59,48 +59,39 @@ Object.keys(opts).forEach(opt => {
59
59
 
60
60
  // disable 'timeout' for debugFlags
61
61
  Object.keys(nodeArgs).forEach(opt => disableTimeouts(opt));
62
+ mochaArgs['node-option'] &&
63
+ mochaArgs['node-option'].forEach(opt => disableTimeouts(opt));
62
64
 
63
65
  // Native debugger handling
64
66
  // see https://nodejs.org/api/debugger.html#debugger_debugger
65
- // look for 'inspect' or 'debug' that would launch this debugger,
67
+ // look for 'inspect' that would launch this debugger,
66
68
  // remove it from Mocha's opts and prepend it to Node's opts.
67
69
  // A deprecation warning will be printed by node, if applicable.
68
70
  // (mochaArgs._ are "positional" arguments, not prefixed with - or --)
69
71
  if (mochaArgs._) {
70
- const i = mochaArgs._.findIndex(val => val === 'inspect' || val === 'debug');
72
+ const i = mochaArgs._.findIndex(val => val === 'inspect');
71
73
  if (i > -1) {
72
- const [command] = mochaArgs._.splice(i, 1);
74
+ mochaArgs._.splice(i, 1);
73
75
  disableTimeouts('inspect');
74
- nodeArgs._ = [command];
76
+ hasInspect = true;
75
77
  }
76
78
  }
77
79
 
78
- // historical
79
- if (nodeArgs.gc) {
80
- deprecate(
81
- '"-gc" is deprecated and will be removed from a future version of Mocha. Use "--gc-global" instead.'
82
- );
83
- nodeArgs['gc-global'] = nodeArgs.gc;
84
- delete nodeArgs.gc;
85
- }
86
-
87
- // --require/-r is treated as Mocha flag except when 'esm' is preloaded
88
- if (mochaArgs.require && mochaArgs.require.includes('esm')) {
89
- nodeArgs.require = ['esm'];
90
- mochaArgs.require = mochaArgs.require.filter(mod => mod !== 'esm');
91
- if (!mochaArgs.require.length) {
92
- delete mochaArgs.require;
93
- }
94
- }
95
-
96
- if (Object.keys(nodeArgs).length) {
80
+ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) {
97
81
  const {spawn} = require('child_process');
98
82
  const mochaPath = require.resolve('../lib/cli/cli.js');
99
83
 
100
- debug('final node args', nodeArgs);
84
+ const nodeArgv =
85
+ (mochaArgs['node-option'] && mochaArgs['node-option'].map(v => '--' + v)) ||
86
+ unparseNodeFlags(nodeArgs);
87
+
88
+ if (hasInspect) nodeArgv.unshift('inspect');
89
+ delete mochaArgs['node-option'];
90
+
91
+ debug('final node argv', nodeArgv);
101
92
 
102
93
  const args = [].concat(
103
- unparseNodeFlags(nodeArgs),
94
+ nodeArgv,
104
95
  mochaPath,
105
96
  unparse(mochaArgs, {alias: aliases})
106
97
  );
@@ -147,5 +138,5 @@ if (Object.keys(nodeArgs).length) {
147
138
  });
148
139
  } else {
149
140
  debug('running Mocha in-process');
150
- require('../lib/cli/cli').main(unparse(mochaArgs, {alias: aliases}));
141
+ require('../lib/cli/cli').main([], mochaArgs);
151
142
  }
package/lib/cli/cli.js CHANGED
@@ -33,8 +33,9 @@ const {cwd} = require('../utils');
33
33
  * @public
34
34
  * @summary Mocha's main command-line entry-point.
35
35
  * @param {string[]} argv - Array of arguments to parse, or by default the lovely `process.argv.slice(2)`
36
+ * @param {object} [mochaArgs] - Object of already parsed Mocha arguments (by bin/mocha)
36
37
  */
37
- exports.main = (argv = process.argv.slice(2)) => {
38
+ exports.main = (argv = process.argv.slice(2), mochaArgs) => {
38
39
  debug('entered main with raw args', argv);
39
40
  // ensure we can require() from current working directory
40
41
  if (typeof module.paths !== 'undefined') {
@@ -43,7 +44,7 @@ exports.main = (argv = process.argv.slice(2)) => {
43
44
 
44
45
  Error.stackTraceLimit = Infinity; // configurable via --stack-trace-limit?
45
46
 
46
- var args = loadOptions(argv);
47
+ var args = mochaArgs || loadOptions(argv);
47
48
 
48
49
  yargs()
49
50
  .scriptName('mocha')
package/lib/cli/config.js CHANGED
@@ -31,7 +31,7 @@ exports.CONFIG_FILES = [
31
31
  ];
32
32
 
33
33
  const isModuleNotFoundError = err =>
34
- err.code !== 'MODULE_NOT_FOUND' ||
34
+ err.code === 'MODULE_NOT_FOUND' ||
35
35
  err.message.indexOf('Cannot find module') !== -1;
36
36
 
37
37
  /**
@@ -49,7 +49,7 @@ exports.isNodeFlag = (flag, bareword = true) => {
49
49
  // and also any V8 flags with `--v8-` prefix
50
50
  (!isMochaFlag(flag) && nodeFlags && nodeFlags.has(flag)) ||
51
51
  debugFlags.has(flag) ||
52
- /(?:preserve-symlinks(?:-main)?|harmony(?:[_-]|$)|(?:trace[_-].+$)|gc(?:[_-]global)?$|es[_-]staging$|use[_-]strict$|v8[_-](?!options).+?$)/.test(
52
+ /(?:preserve-symlinks(?:-main)?|harmony(?:[_-]|$)|(?:trace[_-].+$)|gc[_-]global$|es[_-]staging$|use[_-]strict$|v8[_-](?!options).+?$)/.test(
53
53
  flag
54
54
  )
55
55
  );
@@ -67,7 +67,6 @@ exports.impliesNoTimeouts = flag => debugFlags.has(flag);
67
67
  /**
68
68
  * All non-strictly-boolean arguments to node--those with values--must specify those values using `=`, e.g., `--inspect=0.0.0.0`.
69
69
  * Unparse these arguments using `yargs-unparser` (which would result in `--inspect 0.0.0.0`), then supply `=` where we have values.
70
- * Apparently --require in Node.js v8 does NOT want `=`.
71
70
  * There's probably an easier or more robust way to do this; fixes welcome
72
71
  * @param {Object} opts - Arguments object
73
72
  * @returns {string[]} Unparsed arguments using `=` to specify values
@@ -79,9 +78,7 @@ exports.unparseNodeFlags = opts => {
79
78
  ? args
80
79
  .join(' ')
81
80
  .split(/\b/)
82
- .map((arg, index, args) =>
83
- arg === ' ' && args[index - 1] !== 'require' ? '=' : arg
84
- )
81
+ .map(arg => (arg === ' ' ? '=' : arg))
85
82
  .join('')
86
83
  .split(' ')
87
84
  : [];
@@ -195,10 +195,10 @@ exports.runMocha = async (mocha, options) => {
195
195
  * it actually exists. This must be run _after_ requires are processed (see
196
196
  * {@link handleRequires}), as it'll prevent interfaces from loading otherwise.
197
197
  * @param {Object} opts - Options object
198
- * @param {"reporter"|"interface"} pluginType - Type of plugin.
199
- * @param {Object} [map] - An object perhaps having key `key`. Used as a cache
200
- * of sorts; `Mocha.reporters` is one, where each key corresponds to a reporter
201
- * name
198
+ * @param {"reporter"|"ui"} pluginType - Type of plugin.
199
+ * @param {Object} [map] - Used as a cache of sorts;
200
+ * `Mocha.reporters` where each key corresponds to a reporter name,
201
+ * `Mocha.interfaces` where each key corresponds to an interface name.
202
202
  * @private
203
203
  */
204
204
  exports.validateLegacyPlugin = (opts, pluginType, map = {}) => {
@@ -226,12 +226,12 @@ exports.validateLegacyPlugin = (opts, pluginType, map = {}) => {
226
226
  // if this exists, then it's already loaded, so nothing more to do.
227
227
  if (!map[pluginId]) {
228
228
  try {
229
- opts[pluginType] = require(pluginId);
229
+ map[pluginId] = require(pluginId);
230
230
  } catch (err) {
231
231
  if (err.code === 'MODULE_NOT_FOUND') {
232
232
  // Try to load reporters from a path (absolute or relative)
233
233
  try {
234
- opts[pluginType] = require(path.resolve(pluginId));
234
+ map[pluginId] = require(path.resolve(pluginId));
235
235
  } catch (err) {
236
236
  throw createUnknownError(err);
237
237
  }
@@ -18,6 +18,7 @@ const TYPES = (exports.types = {
18
18
  'file',
19
19
  'global',
20
20
  'ignore',
21
+ 'node-option',
21
22
  'reporter-option',
22
23
  'require',
23
24
  'spec',
@@ -34,6 +35,7 @@ const TYPES = (exports.types = {
34
35
  'diff',
35
36
  'dry-run',
36
37
  'exit',
38
+ 'fail-zero',
37
39
  'forbid-only',
38
40
  'forbid-pending',
39
41
  'full-trace',
@@ -79,6 +81,7 @@ exports.aliases = {
79
81
  invert: ['i'],
80
82
  jobs: ['j'],
81
83
  'no-colors': ['C'],
84
+ 'node-option': ['n'],
82
85
  parallel: ['p'],
83
86
  reporter: ['R'],
84
87
  'reporter-option': ['reporter-options', 'O'],
package/lib/cli/run.js CHANGED
@@ -98,6 +98,10 @@ exports.builder = yargs =>
98
98
  requiresArg: true,
99
99
  coerce: list
100
100
  },
101
+ 'fail-zero': {
102
+ description: 'Fail test run if no test(s) encountered',
103
+ group: GROUPS.RULES
104
+ },
101
105
  fgrep: {
102
106
  conflicts: 'grep',
103
107
  description: 'Only run tests containing this string',
@@ -176,6 +180,10 @@ exports.builder = yargs =>
176
180
  group: GROUPS.OUTPUT,
177
181
  hidden: true
178
182
  },
183
+ 'node-option': {
184
+ description: 'Node or V8 option (no leading "--")',
185
+ group: GROUPS.CONFIG
186
+ },
179
187
  package: {
180
188
  description: 'Path to package.json for config',
181
189
  group: GROUPS.CONFIG,
package/lib/errors.js CHANGED
@@ -328,7 +328,7 @@ function createFatalError(message, value) {
328
328
  /**
329
329
  * Dynamically creates a plugin-type-specific error based on plugin type
330
330
  * @param {string} message - Error message
331
- * @param {"reporter"|"interface"} pluginType - Plugin type. Future: expand as needed
331
+ * @param {"reporter"|"ui"} pluginType - Plugin type. Future: expand as needed
332
332
  * @param {string} [pluginId] - Name/path of plugin, if any
333
333
  * @throws When `pluginType` is not known
334
334
  * @public
@@ -339,7 +339,7 @@ function createInvalidLegacyPluginError(message, pluginType, pluginId) {
339
339
  switch (pluginType) {
340
340
  case 'reporter':
341
341
  return createInvalidReporterError(message, pluginId);
342
- case 'interface':
342
+ case 'ui':
343
343
  return createInvalidInterfaceError(message, pluginId);
344
344
  default:
345
345
  throw new Error('unknown pluginType "' + pluginType + '"');
package/lib/esm-utils.js CHANGED
@@ -34,7 +34,9 @@ const hasStableEsmImplementation = (() => {
34
34
  const [major, minor] = process.version.split('.');
35
35
  // ESM is stable from v12.22.0 onward
36
36
  // https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules
37
- return parseInt(major.slice(1), 10) > 12 || parseInt(minor, 10) >= 22;
37
+ const majorNumber = parseInt(major.slice(1), 10);
38
+ const minorNumber = parseInt(minor, 10);
39
+ return majorNumber > 12 || (majorNumber === 12 && minorNumber >= 22);
38
40
  })();
39
41
 
40
42
  exports.requireOrImport = hasStableEsmImplementation
@@ -47,9 +49,24 @@ exports.requireOrImport = hasStableEsmImplementation
47
49
  } catch (err) {
48
50
  if (
49
51
  err.code === 'ERR_MODULE_NOT_FOUND' ||
50
- err.code === 'ERR_UNKNOWN_FILE_EXTENSION'
52
+ err.code === 'ERR_UNKNOWN_FILE_EXTENSION' ||
53
+ err.code === 'ERR_UNSUPPORTED_DIR_IMPORT'
51
54
  ) {
52
- return require(file);
55
+ try {
56
+ return require(file);
57
+ } catch (requireErr) {
58
+ if (requireErr.code === 'ERR_REQUIRE_ESM') {
59
+ // This happens when the test file is a JS file, but via type:module is actually ESM,
60
+ // AND has an import to a file that doesn't exist.
61
+ // This throws an `ERR_MODULE_NOT_FOUND` // error above,
62
+ // and when we try to `require` it here, it throws an `ERR_REQUIRE_ESM`.
63
+ // What we want to do is throw the original error (the `ERR_MODULE_NOT_FOUND`),
64
+ // and not the `ERR_REQUIRE_ESM` error, which is a red herring.
65
+ throw err;
66
+ } else {
67
+ throw requireErr;
68
+ }
69
+ }
53
70
  } else {
54
71
  throw err;
55
72
  }
package/lib/mocha.js CHANGED
@@ -164,6 +164,7 @@ exports.run = function(...args) {
164
164
  * @param {boolean} [options.delay] - Delay root suite execution?
165
165
  * @param {boolean} [options.diff] - Show diff on failure?
166
166
  * @param {boolean} [options.dryRun] - Report tests without running them?
167
+ * @param {boolean} [options.failZero] - Fail test run if zero tests?
167
168
  * @param {string} [options.fgrep] - Test filter given string.
168
169
  * @param {boolean} [options.forbidOnly] - Tests marked `only` fail the suite?
169
170
  * @param {boolean} [options.forbidPending] - Pending tests fail the suite?
@@ -223,6 +224,7 @@ function Mocha(options = {}) {
223
224
  'delay',
224
225
  'diff',
225
226
  'dryRun',
227
+ 'failZero',
226
228
  'forbidOnly',
227
229
  'forbidPending',
228
230
  'fullTrace',
@@ -581,7 +583,7 @@ Mocha.prototype.fgrep = function(str) {
581
583
  Mocha.prototype.grep = function(re) {
582
584
  if (utils.isString(re)) {
583
585
  // extract args if it's regex-like, i.e: [string, pattern, flag]
584
- var arg = re.match(/^\/(.*)\/(g|i|)$|.*/);
586
+ var arg = re.match(/^\/(.*)\/([gimy]{0,4})$|.*/);
585
587
  this.options.grep = new RegExp(arg[1] || arg[0], arg[2]);
586
588
  } else {
587
589
  this.options.grep = re;
@@ -778,20 +780,6 @@ Mocha.prototype.diff = function(diff) {
778
780
  return this;
779
781
  };
780
782
 
781
- /**
782
- * Enables or disables running tests in dry-run mode.
783
- *
784
- * @public
785
- * @see [CLI option](../#-dry-run)
786
- * @param {boolean} [dryRun=true] - Whether to activate dry-run mode.
787
- * @return {Mocha} this
788
- * @chainable
789
- */
790
- Mocha.prototype.dryRun = function(dryRun) {
791
- this.options.dryRun = dryRun !== false;
792
- return this;
793
- };
794
-
795
783
  /**
796
784
  * @summary
797
785
  * Sets timeout threshold value.
@@ -918,6 +906,34 @@ Mocha.prototype.delay = function delay() {
918
906
  return this;
919
907
  };
920
908
 
909
+ /**
910
+ * Enables or disables running tests in dry-run mode.
911
+ *
912
+ * @public
913
+ * @see [CLI option](../#-dry-run)
914
+ * @param {boolean} [dryRun=true] - Whether to activate dry-run mode.
915
+ * @return {Mocha} this
916
+ * @chainable
917
+ */
918
+ Mocha.prototype.dryRun = function(dryRun) {
919
+ this.options.dryRun = dryRun !== false;
920
+ return this;
921
+ };
922
+
923
+ /**
924
+ * Fails test run if no tests encountered with exit-code 1.
925
+ *
926
+ * @public
927
+ * @see [CLI option](../#-fail-zero)
928
+ * @param {boolean} [failZero=true] - Whether to fail test run.
929
+ * @return {Mocha} this
930
+ * @chainable
931
+ */
932
+ Mocha.prototype.failZero = function(failZero) {
933
+ this.options.failZero = failZero !== false;
934
+ return this;
935
+ };
936
+
921
937
  /**
922
938
  * Causes tests marked `only` to fail the suite.
923
939
  *
@@ -1023,9 +1039,10 @@ Mocha.prototype.run = function(fn) {
1023
1039
  var options = this.options;
1024
1040
  options.files = this.files;
1025
1041
  const runner = new this._runnerClass(suite, {
1042
+ cleanReferencesAfterRun: this._cleanReferencesAfterRun,
1026
1043
  delay: options.delay,
1027
1044
  dryRun: options.dryRun,
1028
- cleanReferencesAfterRun: this._cleanReferencesAfterRun
1045
+ failZero: options.failZero
1029
1046
  });
1030
1047
  createStatsCollector(runner);
1031
1048
  var reporter = new this._reporter(runner, options);
@@ -90,7 +90,7 @@ exports.colors = {
90
90
 
91
91
  exports.symbols = {
92
92
  ok: symbols.success,
93
- err: symbols.err,
93
+ err: symbols.error,
94
94
  dot: '.',
95
95
  comma: ',',
96
96
  bang: '!'
@@ -7,12 +7,16 @@
7
7
  */
8
8
 
9
9
  var Base = require('./base');
10
+ var fs = require('fs');
11
+ var path = require('path');
12
+ const createUnsupportedError = require('../errors').createUnsupportedError;
13
+ const utils = require('../utils');
10
14
  var constants = require('../runner').constants;
11
15
  var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
16
+ var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
12
17
  var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
13
18
  var EVENT_TEST_END = constants.EVENT_TEST_END;
14
19
  var EVENT_RUN_END = constants.EVENT_RUN_END;
15
- var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
16
20
 
17
21
  /**
18
22
  * Expose `JSON`.
@@ -30,7 +34,7 @@ exports = module.exports = JSONReporter;
30
34
  * @param {Runner} runner - Instance triggers reporter actions.
31
35
  * @param {Object} [options] - runner options
32
36
  */
33
- function JSONReporter(runner, options) {
37
+ function JSONReporter(runner, options = {}) {
34
38
  Base.call(this, runner, options);
35
39
 
36
40
  var self = this;
@@ -38,6 +42,14 @@ function JSONReporter(runner, options) {
38
42
  var pending = [];
39
43
  var failures = [];
40
44
  var passes = [];
45
+ var output;
46
+
47
+ if (options.reporterOption && options.reporterOption.output) {
48
+ if (utils.isBrowser()) {
49
+ throw createUnsupportedError('file output not supported in browser');
50
+ }
51
+ output = options.reporterOption.output;
52
+ }
41
53
 
42
54
  runner.on(EVENT_TEST_END, function(test) {
43
55
  tests.push(test);
@@ -66,7 +78,20 @@ function JSONReporter(runner, options) {
66
78
 
67
79
  runner.testResults = obj;
68
80
 
69
- process.stdout.write(JSON.stringify(obj, null, 2));
81
+ var json = JSON.stringify(obj, null, 2);
82
+ if (output) {
83
+ try {
84
+ fs.mkdirSync(path.dirname(output), {recursive: true});
85
+ fs.writeFileSync(output, json);
86
+ } catch (err) {
87
+ console.error(
88
+ `${Base.symbols.err} [mocha] writing output to "${output}" failed: ${err.message}\n`
89
+ );
90
+ process.stdout.write(json);
91
+ }
92
+ } else {
93
+ process.stdout.write(json);
94
+ }
70
95
  });
71
96
  }
72
97
 
package/lib/runner.js CHANGED
@@ -135,10 +135,11 @@ class Runner extends EventEmitter {
135
135
  * @public
136
136
  * @class
137
137
  * @param {Suite} suite - Root suite
138
- * @param {Object|boolean} [opts] - Options. If `boolean` (deprecated), whether or not to delay execution of root suite until ready.
138
+ * @param {Object|boolean} [opts] - Options. If `boolean` (deprecated), whether to delay execution of root suite until ready.
139
+ * @param {boolean} [opts.cleanReferencesAfterRun] - Whether to clean references to test fns and hooks when a suite is done.
139
140
  * @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready.
140
141
  * @param {boolean} [opts.dryRun] - Whether to report tests without running them.
141
- * @param {boolean} [opts.cleanReferencesAfterRun] - Whether to clean references to test fns and hooks when a suite is done.
142
+ * @param {boolean} [options.failZero] - Whether to fail test run if zero tests encountered.
142
143
  */
143
144
  constructor(suite, opts) {
144
145
  super();
@@ -1044,6 +1045,8 @@ Runner.prototype.run = function(fn, opts = {}) {
1044
1045
  fn = fn || function() {};
1045
1046
 
1046
1047
  const end = () => {
1048
+ if (!this.total && this._opts.failZero) this.failures = 1;
1049
+
1047
1050
  debug('run(): root suite completed; emitting %s', constants.EVENT_RUN_END);
1048
1051
  this.emit(constants.EVENT_RUN_END);
1049
1052
  };