From d5dc67414ebcfc8aeb1ac13d692c16194d843781 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Wed, 1 Jul 2020 16:17:06 -0700 Subject: [PATCH] initial implementation of global setup/teardown - adds a generic "plugin loader" which handles root hooks, global setup/teardown, future - renamed `validatePlugin` to `validateLegacyPlugin`. reporters/interfaces should eventually be refactored to use this plugin system - should maybe decouple plugins from the `Mocha` prototype - new events emitted by `Runner` for global setup/teardown - need to add unit tests, need to support parallel mode upgrade eslint-config-semistandard more implementation, tests added more tests, implementation. needs #4366 for browser compat - removed dead code in `lib/cli/run-helpers.js` - be sure not to send certain options to the worker processes - fix root hook bug in worker - send global fixtures to `Runner` constructor - removed debugs - **add `Process.unhandledRejection` listeners** which might be bad - fix some tests while I'm fixing tests reorganize plugin fixtures add "to contain once" mocha result output assertion (to ensure some output is not duplicated) add runGlobalSetup, runGlobalTeardown to Mocha revert changes to Runner `Mocha` will handle global fixtures no events will be emitted remove test/integration/options/require.spec.js which is now in test/integration/plugins/ revert change to ParallelBufferedRunner which invoked global fixtures pull helper code out of test/integration/options/watch.spec.js into integration test helpers - because it's nice to be able to test watching elsewhere - also renamed `runMochaWatch` to `runMochaWatchJSON` and provide `runMochaWatch` for plain output move tests from test/integration/options/require.spec.js into test/integration/plugins/root-hooks.spec.js Mocha#run now runs global fixtures; add watch mode support - add `enableGlobalSetup()` and `enableGlobalTeardown()` methods on `Mocha` - update `globalSetup()` and `globalTeardown()` methods - watch mode runs global teardown upon `SIGINT`. is that right? add command prop to runMochaJSON return value if a fixture run with `runMochaJSON()` fails, the return value now contains a `command` prop (like the result of `runMocha()`) for easier debugging remove unused garbage from RUnner fix bug where mocha would never exit with a non-zero code - I think this is understandable, because `done()` is weirdly _not_ an error-first callback - remove try/catch guards around fixtures and let `process` uncaught/unhandled listeners deal w/ it - also fix `unit/mocha.spec.js` test update package-lock.json move plugin.spec.js into node tests for now remove unused 'watchify', add 'touch' Signed-off-by: Christopher Hiller remove noisy debugs in Runner fix flaky test in test/unit/mocha.spec.js rename assertion types - `JSONRunResult` -> `JSONResult` - `RawRunResult` -> `SummarizedResult` - `RawResult` remains the same - tweak identification functions integration test helper improvements - better organization - renamed stuff for consistency - `runMochaWatch` -> `runMochaWatchAsync` - `splitRegExp` -> `SPLIT_DOT_REPORTER_REGEXP` - rename parameters, add and fix docstrings - modernize some of it - add various typedefs to align with assertion types - remove `invokeSubMocha` as it is no longer needed now that `mocha` will not fork a process if no `node` options are provided - add special case for _forking_ mocha on win32 in watch mode, as that's the only way we can cleanup cleanly (parallel runs will need this treatment too, but I think it demands a more generalized solution) - fix potential bug in `resolveFixturePath()` - in `runMochaWatchAsync`, add `sleepMs` option; defaults to 2s - extract `createTempDir()` from various test files and expose on helper - prefer `rimraf` over `fs-extra.remove` since the former will retry when dir is locked on win32 - expose `touchFile()` on helper; do not use wonky handrolled touching algorithms and use [touch](https://npm.im/touch) instead - update tests to use new helpers and renamed helpers watch improvements and refactors - print "waiting" msg to stderr (_should_ be ok) - add "cleaning up" message upon ctrl-c - remove needless `afterRun` option from various places - only run teardown fixtures if they exist - add win32 fix for testing - add `Mocha#hasGlobalSetupFixtures()` and `Mocha#hasGlobalTeardownFixtures()` more tests Signed-off-by: Christopher Hiller upgrade rewiremock Signed-off-by: Christopher Hiller rename lib/plugin => lib/plugin-loader - rename `createInvalidPluginError` => `createInvalidLegacyPluginError` and soft-deprecate - add `createInvalidPluginDefinitionError` and `createInvalidPluginImplementationError` w/ constants; use them - move some typedefs into `lib/mocha.js` as they are referenced via public aforementioned error factories - remove TS docstrings - better coverage - move `plugin-loader` test back into `test/unit` (removed rewiremock from it) --- lib/cli/run-helpers.js | 69 +- lib/cli/run.js | 14 +- lib/cli/watch-run.js | 79 +- lib/errors.js | 95 +- lib/mocha.js | 232 ++- lib/nodejs/parallel-buffered-runner.js | 18 + lib/nodejs/worker.js | 14 +- lib/plugin-loader.js | 262 +++ lib/runner.js | 62 +- lib/utils.js | 18 + package-lock.json | 1563 ++--------------- package.json | 4 +- test/assertions.js | 145 +- .../global-setup-teardown-multiple.fixture.js | 20 + .../global-setup-teardown.fixture.js | 10 + .../root-hooks}/esm/package.json | 0 .../esm/root-hook-defs-esm.fixture.js | 0 .../root-hooks}/root-hook-defs-a.fixture.js | 0 .../root-hooks}/root-hook-defs-b.fixture.js | 0 .../root-hooks}/root-hook-defs-c.fixture.js | 0 .../root-hooks}/root-hook-defs-d.fixture.js | 0 .../root-hook-defs-esm-broken.fixture.js | 0 .../root-hook-defs-esm.fixture.mjs | 0 .../root-hooks}/root-hook-test-2.fixture.js | 0 .../root-hooks}/root-hook-test.fixture.js | 0 test/integration/helpers.js | 547 ++++-- test/integration/hook-err.spec.js | 4 +- test/integration/hooks.spec.js | 4 +- test/integration/multiple-runs.spec.js | 31 +- test/integration/options/extension.spec.js | 4 +- test/integration/options/watch.spec.js | 194 +- test/integration/pending.spec.js | 4 +- .../plugins/global-setup-teardown.spec.js | 226 +++ .../root-hooks.spec.js} | 44 +- test/integration/retries.spec.js | 4 +- test/node-unit/cli/run-helpers.spec.js | 79 +- test/node-unit/worker.spec.js | 16 +- test/reporters/xunit.spec.js | 28 +- test/unit/mocha.spec.js | 84 +- test/unit/plugin-loader.spec.js | 465 +++++ test/unit/runner.spec.js | 4 - 41 files changed, 2280 insertions(+), 2063 deletions(-) create mode 100644 lib/plugin-loader.js create mode 100644 test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js create mode 100644 test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js rename test/integration/fixtures/{options/require => plugins/root-hooks}/esm/package.json (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/esm/root-hook-defs-esm.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-a.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-b.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-c.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-d.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-esm-broken.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-defs-esm.fixture.mjs (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-test-2.fixture.js (100%) rename test/integration/fixtures/{options/require => plugins/root-hooks}/root-hook-test.fixture.js (100%) create mode 100644 test/integration/plugins/global-setup-teardown.spec.js rename test/integration/{options/require.spec.js => plugins/root-hooks.spec.js} (81%) create mode 100644 test/unit/plugin-loader.spec.js diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index 017d914f4d..4e163db27f 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -12,10 +12,10 @@ const path = require('path'); const debug = require('debug')('mocha:cli:run:helpers'); const {watchRun, watchParallelRun} = require('./watch-run'); const collectFiles = require('./collect-files'); -const {type} = require('../utils'); const {format} = require('util'); -const {createInvalidPluginError, createUnsupportedError} = require('../errors'); +const {createInvalidLegacyPluginError} = require('../errors'); const {requireOrImport} = require('../esm-utils'); +const PluginLoader = require('../plugin-loader'); /** * Exits Mocha when tests + code under test has finished execution (default) @@ -79,12 +79,12 @@ exports.list = str => * * Returns array of `mochaHooks` exports, if any. * @param {string[]} requires - Modules to require - * @returns {Promise} Any root hooks + * @returns {Promise} Plugin implementations * @private */ exports.handleRequires = async (requires = []) => { - const acc = []; - for (const mod of requires) { + const pluginLoader = PluginLoader.create(); + for await (const mod of requires) { let modpath = mod; // this is relative to cwd if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) { @@ -92,49 +92,18 @@ exports.handleRequires = async (requires = []) => { debug('resolved required file %s to %s', mod, modpath); } const requiredModule = await requireOrImport(modpath); - if ( - requiredModule && - typeof requiredModule === 'object' && - requiredModule.mochaHooks - ) { - const mochaHooksType = type(requiredModule.mochaHooks); - if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') { - debug('found root hooks in required file %s', mod); - acc.push(requiredModule.mochaHooks); - } else { - throw createUnsupportedError( - 'mochaHooks must be an object or a function returning (or fulfilling with) an object' - ); + if (requiredModule && typeof requiredModule === 'object') { + if (pluginLoader.load(requiredModule)) { + debug('found one or more plugin implementations in %s', modpath); } } debug('loaded required module "%s"', mod); } - return acc; -}; - -/** - * Loads root hooks as exported via `mochaHooks` from required files. - * These can be sync/async functions returning objects, or just objects. - * Flattens to a single object. - * @param {Array} rootHooks - Array of root hooks - * @private - * @returns {MochaRootHookObject} - */ -exports.loadRootHooks = async rootHooks => { - const rootHookObjects = await Promise.all( - rootHooks.map(async hook => (/function$/.test(type(hook)) ? hook() : hook)) - ); - - return rootHookObjects.reduce( - (acc, hook) => { - acc.beforeAll = acc.beforeAll.concat(hook.beforeAll || []); - acc.beforeEach = acc.beforeEach.concat(hook.beforeEach || []); - acc.afterAll = acc.afterAll.concat(hook.afterAll || []); - acc.afterEach = acc.afterEach.concat(hook.afterEach || []); - return acc; - }, - {beforeAll: [], beforeEach: [], afterAll: [], afterEach: []} - ); + const plugins = await pluginLoader.finalize(); + if (Object.keys(plugins).length) { + debug('finalized plugin implementations: %O', plugins); + } + return plugins; }; /** @@ -236,7 +205,7 @@ exports.runMocha = async (mocha, options) => { * name * @private */ -exports.validatePlugin = (opts, pluginType, map = {}) => { +exports.validateLegacyPlugin = (opts, pluginType, map = {}) => { /** * This should be a unique identifier; either a string (present in `map`), * or a resolvable (via `require.resolve`) module ID/path. @@ -245,14 +214,14 @@ exports.validatePlugin = (opts, pluginType, map = {}) => { const pluginId = opts[pluginType]; if (Array.isArray(pluginId)) { - throw createInvalidPluginError( + throw createInvalidLegacyPluginError( `"--${pluginType}" can only be specified once`, pluginType ); } - const unknownError = err => - createInvalidPluginError( + const createUnknownError = err => + createInvalidLegacyPluginError( format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err), pluginType, pluginId @@ -268,10 +237,10 @@ exports.validatePlugin = (opts, pluginType, map = {}) => { try { opts[pluginType] = require(path.resolve(pluginId)); } catch (err) { - throw unknownError(err); + throw createUnknownError(err); } } else { - throw unknownError(err); + throw createUnknownError(err); } } } diff --git a/lib/cli/run.js b/lib/cli/run.js index 6582a4e2c5..a8c8b619b3 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -19,8 +19,7 @@ const { const { list, handleRequires, - validatePlugin, - loadRootHooks, + validateLegacyPlugin, runMocha } = require('./run-helpers'); const {ONE_AND_DONES, ONE_AND_DONE_ARGS} = require('./one-and-dones'); @@ -339,13 +338,10 @@ exports.builder = yargs => // currently a failing middleware does not work nicely with yargs' `fail()`. try { // load requires first, because it can impact "plugin" validation - const rawRootHooks = await handleRequires(argv.require); - validatePlugin(argv, 'reporter', Mocha.reporters); - validatePlugin(argv, 'ui', Mocha.interfaces); - - if (rawRootHooks && rawRootHooks.length) { - argv.rootHooks = await loadRootHooks(rawRootHooks); - } + const plugins = await handleRequires(argv.require); + validateLegacyPlugin(argv, 'reporter', Mocha.reporters); + validateLegacyPlugin(argv, 'ui', Mocha.interfaces); + Object.assign(argv, plugins); } catch (err) { // this could be a bad --require, bad reporter, ui, etc. console.error(`\n${symbols.error} ${ansi.red('ERROR:')}`, err); diff --git a/lib/cli/watch-run.js b/lib/cli/watch-run.js index d36a58394e..3bac550389 100644 --- a/lib/cli/watch-run.js +++ b/lib/cli/watch-run.js @@ -1,5 +1,6 @@ 'use strict'; +const logSymbols = require('log-symbols'); const debug = require('debug')('mocha:cli:watch'); const path = require('path'); const chokidar = require('chokidar'); @@ -32,6 +33,7 @@ exports.watchParallelRun = ( fileCollectParams ) => { debug('creating parallel watcher'); + return createWatcher(mocha, { watchFiles, watchIgnore, @@ -68,9 +70,6 @@ exports.watchParallelRun = ( newMocha.lazyLoadFiles(true); return newMocha; }, - afterRun({watcher}) { - blastCache(watcher); - }, fileCollectParams }); }; @@ -91,7 +90,6 @@ exports.watchParallelRun = ( */ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { debug('creating serial watcher'); - // list of all test files return createWatcher(mocha, { watchFiles, @@ -128,9 +126,6 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { return newMocha; }, - afterRun({watcher}) { - blastCache(watcher); - }, fileCollectParams }); }; @@ -141,7 +136,6 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { * @param {Object} opts * @param {BeforeWatchRun} [opts.beforeRun] - Function to call before * `mocha.run()` - * @param {AfterWatchRun} [opts.afterRun] - Function to call after `mocha.run()` * @param {string[]} [opts.watchFiles] - List of paths and patterns to watch. If * not provided all files with an extension included in * `fileCollectionParams.extension` are watched. See first argument of @@ -155,13 +149,17 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { */ const createWatcher = ( mocha, - {watchFiles, watchIgnore, beforeRun, afterRun, fileCollectParams} + {watchFiles, watchIgnore, beforeRun, fileCollectParams} ) => { if (!watchFiles) { watchFiles = fileCollectParams.extension.map(ext => `**/*.${ext}`); } debug('ignoring files matching: %s', watchIgnore); + let globalFixtureContext; + + // we handle global fixtures manually + mocha.enableGlobalSetup(false).enableGlobalTeardown(false); const watcher = chokidar.watch(watchFiles, { ignored: watchIgnore, @@ -169,11 +167,14 @@ const createWatcher = ( }); const rerunner = createRerunner(mocha, watcher, { - beforeRun, - afterRun + beforeRun }); - watcher.on('ready', () => { + watcher.on('ready', async () => { + if (!globalFixtureContext) { + debug('triggering global setup'); + globalFixtureContext = await mocha.runGlobalSetup(); + } rerunner.run(); }); @@ -185,10 +186,39 @@ const createWatcher = ( process.on('exit', () => { showCursor(); }); - process.on('SIGINT', () => { + + // this is for testing. + // win32 cannot gracefully shutdown via a signal from a parent + // process; a `SIGINT` from a parent will cause the process + // to immediately exit. during normal course of operation, a user + // will type Ctrl-C and the listener will be invoked, but this + // is not possible in automated testing. + // there may be another way to solve this, but it too will be a hack. + // for our watch tests on win32 we must _fork_ mocha with an IPC channel + if (process.connected) { + process.on('message', msg => { + if (msg === 'SIGINT') { + process.emit('SIGINT'); + } + }); + } + + let exiting = false; + process.on('SIGINT', async () => { showCursor(); - console.log('\n'); - process.exit(128 + 2); + console.error(`${logSymbols.warning} [mocha] cleaning up, please wait...`); + if (!exiting) { + exiting = true; + if (mocha.hasGlobalTeardownFixtures()) { + debug('running global teardown'); + try { + await mocha.runGlobalTeardown(globalFixtureContext); + } catch (err) { + console.error(err); + } + } + process.exit(130); + } }); // Keyboard shortcut for restarting when "rs\n" is typed (ala Nodemon) @@ -212,12 +242,11 @@ const createWatcher = ( * @param {FSWatcher} watcher - chokidar `FSWatcher` instance * @param {Object} [opts] - Options! * @param {BeforeWatchRun} [opts.beforeRun] - Function to call before `mocha.run()` - * @param {AfterWatchRun} [opts.afterRun] - Function to call after `mocha.run()` * @returns {Rerunner} * @ignore * @private */ -const createRerunner = (mocha, watcher, {beforeRun, afterRun} = {}) => { +const createRerunner = (mocha, watcher, {beforeRun} = {}) => { // Set to a `Runner` when mocha is running. Set to `null` when mocha is not // running. let runner = null; @@ -226,16 +255,15 @@ const createRerunner = (mocha, watcher, {beforeRun, afterRun} = {}) => { let rerunScheduled = false; const run = () => { - mocha = beforeRun ? beforeRun({mocha, watcher}) : mocha; - + mocha = beforeRun ? beforeRun({mocha, watcher}) || mocha : mocha; runner = mocha.run(() => { debug('finished watch run'); runner = null; - afterRun && afterRun({mocha, watcher}); + blastCache(watcher); if (rerunScheduled) { rerun(); } else { - debug('waiting for changes...'); + console.error(`${logSymbols.info} [mocha] waiting for changes...`); } }); }; @@ -333,15 +361,6 @@ const blastCache = watcher => { * @returns {Mocha} */ -/** - * Callback to be run after `mocha.run()` completes. Typically used to clear - * require cache. - * @callback AfterWatchRun - * @private - * @param {{mocha: Mocha, watcher: FSWatcher}} options - * @returns {void} - */ - /** * Object containing run control methods * @typedef {Object} Rerunner diff --git a/lib/errors.js b/lib/errors.js index 56e01c04c3..b37154475b 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,6 +1,7 @@ 'use strict'; var format = require('util').format; +const {deprecate} = require('./utils'); /** * Factory functions to create throwable error objects @@ -71,7 +72,17 @@ var constants = { /** * Use of `only()` w/ `--forbid-only` results in this error. */ - FORBIDDEN_EXCLUSIVITY: 'ERR_MOCHA_FORBIDDEN_EXCLUSIVITY' + FORBIDDEN_EXCLUSIVITY: 'ERR_MOCHA_FORBIDDEN_EXCLUSIVITY', + + /** + * To be thrown when a user-defined plugin implementation (e.g., `mochaHooks`) is invalid + */ + INVALID_PLUGIN_IMPLEMENTATION: 'ERR_MOCHA_INVALID_PLUGIN_IMPLEMENTATION', + + /** + * To be thrown when a builtin or third-party plugin definition (the _definition_ of `mochaHooks`) is invalid + */ + INVALID_PLUGIN_DEFINITION: 'ERR_MOCHA_INVALID_PLUGIN_DEFINITION' }; /** @@ -221,7 +232,7 @@ function createFatalError(message, value) { * @public * @returns {Error} */ -function createInvalidPluginError(message, pluginType, pluginId) { +function createInvalidLegacyPluginError(message, pluginType, pluginId) { switch (pluginType) { case 'reporter': return createInvalidReporterError(message, pluginId); @@ -232,6 +243,21 @@ function createInvalidPluginError(message, pluginType, pluginId) { } } +/** + * **DEPRECATED**. Use {@link createInvalidLegacyPluginError} instead Dynamically creates a plugin-type-specific error based on plugin type + * @deprecated + * @param {string} message - Error message + * @param {"reporter"|"interface"} pluginType - Plugin type. Future: expand as needed + * @param {string} [pluginId] - Name/path of plugin, if any + * @throws When `pluginType` is not known + * @public + * @returns {Error} + */ +function createInvalidPluginError(...args) { + deprecate('Use createInvalidLegacyPluginError() instead'); + return createInvalidLegacyPluginError(...args); +} + /** * Creates an error object to be thrown when a mocha object's `run` method is executed while it is already disposed. * @param {string} message The error message to be displayed. @@ -315,20 +341,55 @@ function createForbiddenExclusivityError(mocha) { return err; } +/** + * Creates an error object to be thrown when a plugin definition is invalid + * @param {string} msg - Error message + * @param {PluginDefinition} [pluginDef] - Problematic plugin definition + * @public + * @returns {Error} Error with code {@link constants.INVALID_PLUGIN_DEFINITION} + */ +function createInvalidPluginDefinitionError(msg, pluginDef) { + const err = new Error(msg); + err.code = constants.INVALID_PLUGIN_DEFINITION; + err.pluginDef = pluginDef; + return err; +} + +/** + * Creates an error object to be thrown when a plugin implementation (user code) is invalid + * @param {string} msg - Error message + * @param {{pluginDef?: PluginDefinition, pluginImpl?: *}} [opts] - Plugin definition and user-supplied implementation + * @public + * @returns {Error} Error with code {@link constants.INVALID_PLUGIN_DEFINITION} + */ +function createInvalidPluginImplementationError( + msg, + {pluginDef, pluginImpl} = {} +) { + const err = new Error(msg); + err.code = constants.INVALID_PLUGIN_IMPLEMENTATION; + err.pluginDef = pluginDef; + err.pluginImpl = pluginImpl; + return err; +} + module.exports = { - createInvalidArgumentTypeError: createInvalidArgumentTypeError, - createInvalidArgumentValueError: createInvalidArgumentValueError, - createInvalidExceptionError: createInvalidExceptionError, - createInvalidInterfaceError: createInvalidInterfaceError, - createInvalidReporterError: createInvalidReporterError, - createMissingArgumentError: createMissingArgumentError, - createNoFilesMatchPatternError: createNoFilesMatchPatternError, - createUnsupportedError: createUnsupportedError, - createInvalidPluginError: createInvalidPluginError, - createMochaInstanceAlreadyDisposedError: createMochaInstanceAlreadyDisposedError, - createMochaInstanceAlreadyRunningError: createMochaInstanceAlreadyRunningError, - createFatalError: createFatalError, - createMultipleDoneError: createMultipleDoneError, - createForbiddenExclusivityError: createForbiddenExclusivityError, - constants: constants + constants, + createFatalError, + createForbiddenExclusivityError, + createInvalidArgumentTypeError, + createInvalidArgumentValueError, + createInvalidExceptionError, + createInvalidInterfaceError, + createInvalidPluginDefinitionError, + createInvalidPluginImplementationError, + createInvalidPluginError, + createInvalidLegacyPluginError, + createInvalidReporterError, + createMissingArgumentError, + createMochaInstanceAlreadyDisposedError, + createMochaInstanceAlreadyRunningError, + createMultipleDoneError, + createNoFilesMatchPatternError, + createUnsupportedError }; diff --git a/lib/mocha.js b/lib/mocha.js index 0b4aa5a4bd..12362d66f5 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -12,21 +12,23 @@ var builtinReporters = require('./reporters'); var growl = require('./nodejs/growl'); var utils = require('./utils'); var mocharc = require('./mocharc.json'); -var errors = require('./errors'); var Suite = require('./suite'); var esmUtils = utils.supportsEsModules(true) ? require('./esm-utils') : undefined; var createStatsCollector = require('./stats-collector'); -var createInvalidReporterError = errors.createInvalidReporterError; -var createInvalidInterfaceError = errors.createInvalidInterfaceError; -var createMochaInstanceAlreadyDisposedError = - errors.createMochaInstanceAlreadyDisposedError; -var createMochaInstanceAlreadyRunningError = - errors.createMochaInstanceAlreadyRunningError; -var EVENT_FILE_PRE_REQUIRE = Suite.constants.EVENT_FILE_PRE_REQUIRE; -var EVENT_FILE_POST_REQUIRE = Suite.constants.EVENT_FILE_POST_REQUIRE; -var EVENT_FILE_REQUIRE = Suite.constants.EVENT_FILE_REQUIRE; +const { + createInvalidReporterError, + createInvalidInterfaceError, + createMochaInstanceAlreadyDisposedError, + createMochaInstanceAlreadyRunningError, + createUnsupportedError +} = require('./errors'); +const { + EVENT_FILE_PRE_REQUIRE, + EVENT_FILE_POST_REQUIRE, + EVENT_FILE_REQUIRE +} = Suite.constants; var sQuote = utils.sQuote; var debug = require('debug')('mocha:mocha'); @@ -205,6 +207,11 @@ function Mocha(options) { */ this.isWorker = Boolean(options.isWorker); + this.globalSetup(options.globalSetup) + .globalTeardown(options.globalTeardown) + .enableGlobalSetup(options.enableGlobalSetup) + .enableGlobalTeardown(options.enableGlobalTeardown); + if ( options.parallel && (typeof options.jobs === 'undefined' || options.jobs > 1) @@ -1006,7 +1013,28 @@ Mocha.prototype.run = function(fn) { } } - return runner.run(done, {files: this.files, options: options}); + (async () => { + if (this.options.enableGlobalSetup && this.hasGlobalSetupFixtures()) { + debug('run(): running global setup'); + const context = await this.runGlobalSetup(runner); + runner.run( + async failures => { + if ( + this.options.enableGlobalTeardown && + this.hasGlobalTeardownFixtures() + ) { + debug('run(): running global teardown'); + await this.runGlobalTeardown(runner, {context}); + } + done(failures); + }, + {files: this.files, options} + ); + } else { + runner.run(done, {files: this.files, options}); + } + })(); + return runner; }; /** @@ -1051,9 +1079,7 @@ Mocha.prototype.rootHooks = function rootHooks(hooks) { */ Mocha.prototype.parallelMode = function parallelMode(enable) { if (utils.isBrowser()) { - throw errors.createUnsupportedError( - 'parallel mode is only supported in Node.js' - ); + throw createUnsupportedError('parallel mode is only supported in Node.js'); } var parallel = enable === true; if ( @@ -1064,7 +1090,7 @@ Mocha.prototype.parallelMode = function parallelMode(enable) { return this; } if (this._state !== mochaStates.INIT) { - throw errors.createUnsupportedError( + throw createUnsupportedError( 'cannot change parallel mode after having called run()' ); } @@ -1097,8 +1123,145 @@ Mocha.prototype.lazyLoadFiles = function lazyLoadFiles(enable) { }; /** - * An alternative way to define root hooks that works with parallel runs. + * Configures one or more global setup fixtures. + * + * If given no parameters, _unsets_ any previously-set fixtures. + * @chainable + * @public + * @param {MochaGlobalFixture|MochaGlobalFixture[]} [setupFns] - Global setup fixture(s) + * @returns {Mocha} + */ +Mocha.prototype.globalSetup = function globalSetup(setupFns = []) { + setupFns = utils.castArray(setupFns); + this.options.globalSetup = setupFns; + debug('configured %d global setup functions', setupFns.length); + return this; +}; + +/** + * Configures one or more global teardown fixtures. + * + * If given no parameters, _unsets_ any previously-set fixtures. + * @chainable + * @public + * @param {MochaGlobalFixture|MochaGlobalFixture[]} [teardownFns] - Global teardown fixture(s) + * @returns {Mocha} + */ +Mocha.prototype.globalTeardown = function globalTeardown(teardownFns = []) { + teardownFns = utils.castArray(teardownFns); + this.options.globalTeardown = teardownFns; + debug('configured %d global teardown functions', teardownFns.length); + return this; +}; + +/** + * Run any global setup fixtures sequentially. + * + * This is called by {@link Mocha#run} _unless_ the `runGlobalSetup` option is `false`. + * + * The context object this function resolves with should be consumed by + * {@link Mocha#runGlobalTeardown}. + * @param {object} [context] - Context object if already have one + * @private + * @returns {Promise} Context object + */ +Mocha.prototype.runGlobalSetup = async function runGlobalSetup(context = {}) { + const {globalSetup} = this.options; + if (globalSetup && globalSetup.length) { + debug('run(): global setup starting'); + await this._runGlobalFixtures(globalSetup, context); + debug('run(): global setup complete'); + } + return context; +}; + +/** + * Run any global teardown fixtures sequentially. + * + * This is called by {@link Mocha#run} _unless_ the `runGlobalTeardown` option is `false`. + * + * SHould be called with `context` returned by {@link Mocha#runGlobalSetup}. + * @param {object} [context] - Context object if already have one * @private + * @returns {Promise} Context object + */ +Mocha.prototype.runGlobalTeardown = async function runGlobalTeardown( + context = {} +) { + const {globalTeardown} = this.options; + if (globalTeardown && globalTeardown.length) { + debug('run(): global teardown starting'); + await this._runGlobalFixtures(globalTeardown, context); + } + debug('run(): global teardown complete'); + return context; +}; + +/** + * Run global fixtures sequentially with context `context` + * @private + * @param {MochaGlobalFixture[]} [fixtureFns] - Fixtures to run + * @param {object} [context] - context object + * @returns {Promise} context object + */ +Mocha.prototype._runGlobalFixtures = async function _runGlobalFixtures( + fixtureFns = [], + context = {} +) { + for await (const fixtureFn of fixtureFns) { + await fixtureFn.call(context); + } + return context; +}; + +/** + * Toggle execution of any global setup fixture(s) + * + * @chainable + * @public + * @param {boolean } [enabled=true] - If `false`, do not run global setup fixture + * @returns {Mocha} + */ +Mocha.prototype.enableGlobalSetup = function enableGlobalSetup(enabled = true) { + this.options.enableGlobalSetup = Boolean(enabled); + return this; +}; + +/** + * Toggle execution of any global teardown fixture(s) + * + * @chainable + * @public + * @param {boolean } [enabled=true] - If `false`, do not run global teardown fixture + * @returns {Mocha} + */ +Mocha.prototype.enableGlobalTeardown = function enableGlobalTeardown( + enabled = true +) { + this.options.enableGlobalTeardown = Boolean(enabled); + return this; +}; + +/** + * Returns `true` if one or more global setup fixtures have been supplied. + * @public + * @returns {boolean} + */ +Mocha.prototype.hasGlobalSetupFixtures = function hasGlobalSetupFixtures() { + return Boolean(this.options.globalSetup.length); +}; + +/** + * Returns `true` if one or more global teardown fixtures have been supplied. + * @public + * @returns {boolean} + */ +Mocha.prototype.hasGlobalTeardownFixtures = function hasGlobalTeardownFixtures() { + return Boolean(this.options.globalTeardown.length); +}; + +/** + * An alternative way to define root hooks that works with parallel runs. * @typedef {Object} MochaRootHookObject * @property {Function|Function[]} [beforeAll] - "Before all" hook(s) * @property {Function|Function[]} [beforeEach] - "Before each" hook(s) @@ -1108,7 +1271,40 @@ Mocha.prototype.lazyLoadFiles = function lazyLoadFiles(enable) { /** * An function that returns a {@link MochaRootHookObject}, either sync or async. - * @private - * @callback MochaRootHookFunction + @callback MochaRootHookFunction * @returns {MochaRootHookObject|Promise} */ + +/** + * A function that's invoked _once_ which is either sync or async. + * Can be a "teardown" or "setup". These will all share the same context. + * @callback MochaGlobalFixture + * @returns {void|Promise} + */ + +/** + * An object making up all necessary parts of a plugin loader and aggregator + * @typedef {Object} PluginDefinition + * @property {string} exportName - Named export to use + * @property {string} [optionName] - Option name for Mocha constructor (use `exportName` if omitted) + * @property {PluginValidator} [validate] - Validator function + * @property {PluginFinalizer} [finalize] - Finalizer/aggregator function + */ + +/** + * A (sync) function to assert a user-supplied plugin implementation is valid. + * + * Defined in a {@link PluginDefinition}. + + * @callback PluginValidator + * @param {*} value - Value to check + * @this {PluginDefinition} + * @returns {void} + */ + +/** + * A function to finalize plugins impls of a particular ilk + * @callback PluginFinalizer + * @param {*[]} impls - User-supplied implementations + * @returns {Promise<*>|*} + */ diff --git a/lib/nodejs/parallel-buffered-runner.js b/lib/nodejs/parallel-buffered-runner.js index ee8635ab98..a079bbd571 100644 --- a/lib/nodejs/parallel-buffered-runner.js +++ b/lib/nodejs/parallel-buffered-runner.js @@ -14,6 +14,18 @@ const {BufferedWorkerPool} = require('./buffered-worker-pool'); const {setInterval, clearInterval} = global; const {createMap} = require('../utils'); +/** + * List of options to _not_ serialize for transmission to workers + */ +const DENY_OPTIONS = [ + 'globalSetup', + 'globalTeardown', + 'parallel', + 'p', + 'jobs', + 'j' +]; + /** * Outputs a debug statement with worker stats * @param {BufferedWorkerPool} pool - Worker pool @@ -235,6 +247,11 @@ class ParallelBufferedRunner extends Runner { this.emit(EVENT_RUN_BEGIN); + options = {...options}; + DENY_OPTIONS.forEach(opt => { + delete options[opt]; + }); + const results = await allSettled( files.map(this._createFileRunner(pool, options)) ); @@ -257,6 +274,7 @@ class ParallelBufferedRunner extends Runner { if (this._state === ABORTING) { return; } + this.emit(EVENT_RUN_END); debug('run(): completing with failure count %d', this.failures); callback(this.failures); diff --git a/lib/nodejs/worker.js b/lib/nodejs/worker.js index 81abb6bb15..5139b64ee3 100644 --- a/lib/nodejs/worker.js +++ b/lib/nodejs/worker.js @@ -12,11 +12,7 @@ const { } = require('../errors'); const workerpool = require('workerpool'); const Mocha = require('../mocha'); -const { - handleRequires, - validatePlugin, - loadRootHooks -} = require('../cli/run-helpers'); +const {handleRequires, validateLegacyPlugin} = require('../cli/run-helpers'); const d = require('debug'); const debug = d.debug(`mocha:parallel:worker:${process.pid}`); const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`); @@ -45,9 +41,11 @@ if (workerpool.isMainThread) { * @param {Options} argv - Command-line options */ let bootstrap = async argv => { - const rawRootHooks = await handleRequires(argv.require); - rootHooks = await loadRootHooks(rawRootHooks); - validatePlugin(argv, 'ui', Mocha.interfaces); + const plugins = await handleRequires(argv.require); + validateLegacyPlugin(argv, 'ui', Mocha.interfaces); + + // globalSetup and globalTeardown do not run in workers + rootHooks = plugins.rootHooks; bootstrap = () => {}; debug('bootstrap(): finished with args: %O', argv); }; diff --git a/lib/plugin-loader.js b/lib/plugin-loader.js new file mode 100644 index 0000000000..2887f229b6 --- /dev/null +++ b/lib/plugin-loader.js @@ -0,0 +1,262 @@ +/** + * Provides a way to load "plugins" as provided by the user. + * + * Currently supports: + * + * - Root hooks + * - Global fixtures (setup/teardown) + * @private + * @module plugin + */ + +'use strict'; + +const debug = require('debug')('mocha:plugin-loader'); +const { + createInvalidPluginDefinitionError, + createInvalidPluginImplementationError +} = require('./errors'); +const {castArray} = require('./utils'); + +/** + * Built-in plugin definitions. + */ +const MochaPlugins = [ + /** + * Root hook plugin definition + * @type {PluginDefinition} + */ + { + exportName: 'mochaHooks', + optionName: 'rootHooks', + validate(value) { + if ( + Array.isArray(value) || + (typeof value !== 'function' && typeof value !== 'object') + ) { + throw createInvalidPluginImplementationError( + `mochaHooks must be an object or a function returning (or fulfilling with) an object` + ); + } + }, + async finalize(rootHooks) { + if (rootHooks.length) { + const rootHookObjects = await Promise.all( + rootHooks.map(async hook => + typeof hook === 'function' ? hook() : hook + ) + ); + + return rootHookObjects.reduce( + (acc, hook) => { + hook = { + beforeAll: [], + beforeEach: [], + afterAll: [], + afterEach: [], + ...hook + }; + return { + beforeAll: [...acc.beforeAll, ...castArray(hook.beforeAll)], + beforeEach: [...acc.beforeEach, ...castArray(hook.beforeEach)], + afterAll: [...acc.afterAll, ...castArray(hook.afterAll)], + afterEach: [...acc.afterEach, ...castArray(hook.afterEach)] + }; + }, + {beforeAll: [], beforeEach: [], afterAll: [], afterEach: []} + ); + } + } + }, + /** + * Global setup fixture plugin definition + * @type {PluginDefinition} + */ + { + exportName: 'mochaGlobalSetup', + optionName: 'globalSetup', + validate(value) { + let isValid = true; + if (Array.isArray(value)) { + if (value.some(item => typeof item !== 'function')) { + isValid = false; + } + } else if (typeof value !== 'function') { + isValid = false; + } + if (!isValid) { + throw createInvalidPluginImplementationError( + `mochaGlobalSetup must be a function or an array of functions`, + {pluginDef: this, pluginImpl: value} + ); + } + } + }, + /** + * Global teardown fixture plugin definition + * @type {PluginDefinition} + */ + { + exportName: 'mochaGlobalTeardown', + optionName: 'globalTeardown', + validate(value) { + let isValid = true; + if (Array.isArray(value)) { + if (value.some(item => typeof item !== 'function')) { + isValid = false; + } + } else if (typeof value !== 'function') { + isValid = false; + } + if (!isValid) { + throw createInvalidPluginImplementationError( + `mochaGlobalTeardown must be a function or an array of functions`, + {pluginDef: this, pluginImpl: value} + ); + } + } + } +]; + +/** + * Contains a registry of [plugin definitions]{@link PluginDefinition} and discovers plugin implementations in user-supplied code. + * + * - [load()]{@link #load} should be called for all required modules + * - The result of [finalize()]{@link #finalize} should be merged into the options for the [Mocha]{@link Mocha} constructor. + * @private + */ +class PluginLoader { + /** + * Initializes plugin names, plugin map, etc. + * @param {PluginDefinition[]} [pluginDefinitions=MochaPlugins] - Plugin definitions + */ + constructor(pluginDefinitions = MochaPlugins) { + /** + * Map of registered plugin defs + * @type {Map} + */ + this.registered = new Map(); + + /** + * Cache of known `optionName` values for checking conflicts + * @type {Set} + */ + this.knownOptionNames = new Set(); + + /** + * Cache of known `exportName` values for checking conflicts + * @type {Set} + */ + this.knownExportNames = new Set(); + + /** + * Map of user-supplied plugin implementations + * @type {Map} + */ + this.loaded = new Map(); + + pluginDefinitions.forEach(pluginDef => { + this.register(pluginDef); + }); + + debug('registered %d plugin defs', this.registered.size); + } + + /** + * Register a plugin + * @param {PluginDefinition} pluginDef - Plugin definition + */ + register(pluginDef) { + if (!pluginDef || typeof pluginDef !== 'object') { + throw createInvalidPluginDefinitionError( + 'pluginDef is non-object or falsy', + pluginDef + ); + } + if (!pluginDef.exportName) { + throw createInvalidPluginDefinitionError( + `exportName is expected to be a non-empty string`, + pluginDef + ); + } + let {exportName} = pluginDef; + exportName = String(exportName); + pluginDef.optionName = String(pluginDef.optionName || exportName); + if (this.knownExportNames.has(exportName)) { + throw createInvalidPluginDefinitionError( + `Plugin definition conflict: ${exportName}; exportName must be unique`, + pluginDef + ); + } + this.loaded.set(exportName, []); + this.registered.set(exportName, pluginDef); + this.knownExportNames.add(exportName); + this.knownOptionNames.add(pluginDef.optionName); + debug('registered plugin def "%s"', exportName); + } + + /** + * Inspects a module's exports for known plugins and keeps them in memory. + * + * @param {*} requiredModule - The exports of a module loaded via `--require` + * @returns {boolean} If one or more plugins was found, return `true`. + */ + load(requiredModule) { + // we should explicitly NOT fail if other stuff is exported. + // we only care about the plugins we know about. + if (requiredModule && typeof requiredModule === 'object') { + return Array.from(this.knownExportNames).reduce( + (pluginImplFound, pluginName) => { + const pluginImpl = requiredModule[pluginName]; + if (pluginImpl) { + const plugin = this.registered.get(pluginName); + if (typeof plugin.validate === 'function') { + plugin.validate(pluginImpl); + } + this.loaded.set(pluginName, [ + ...this.loaded.get(pluginName), + ...castArray(pluginImpl) + ]); + return true; + } + return pluginImplFound; + }, + false + ); + } + return false; + } + + /** + * Call the `finalize()` function of each known plugin definition on the plugins found by [load()]{@link PluginLoader#load}. + * + * Output suitable for passing as input into {@link Mocha} constructor. + * @returns {Promise} Object having keys corresponding to registered plugin definitions' `optionName` prop (or `exportName`, if none), and the values are the implementations as provided by a user. + */ + async finalize() { + const finalizedPlugins = Object.create(null); + + for await (const [exportName, pluginImpls] of this.loaded.entries()) { + if (pluginImpls.length) { + const plugin = this.registered.get(exportName); + finalizedPlugins[plugin.optionName] = + typeof plugin.finalize === 'function' + ? await plugin.finalize(pluginImpls) + : pluginImpls; + } + } + + debug('finalized plugins: %O', finalizedPlugins); + return finalizedPlugins; + } + + /** + * Constructs a {@link PluginLoader} + * @param {PluginDefinition[]} [pluginDefs] - Plugin definitions + */ + static create(pluginDefs = MochaPlugins) { + return new PluginLoader(pluginDefs); + } +} + +module.exports = PluginLoader; diff --git a/lib/runner.js b/lib/runner.js index 876d64fb24..f6d4ea499c 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -146,6 +146,7 @@ function Runner(suite, opts) { opts = {}; } if (typeof opts === 'boolean') { + // TODO: deprecate this this._delay = opts; opts = {}; } else { @@ -978,66 +979,71 @@ Runner.prototype._uncaught = function(err) { * @param {{files: string[], options: Options}} [opts] - For subclasses * @return {Runner} Runner instance. */ -Runner.prototype.run = function(fn, opts) { - var self = this; +Runner.prototype.run = function(fn, opts = {}) { var rootSuite = this.suite; + var options = opts.options || {}; + debug('run(): got options: %O', options); fn = fn || function() {}; - function start() { + const end = () => { + debug('run(): root suite completed; emitting %s', constants.EVENT_RUN_END); + this.emit(constants.EVENT_RUN_END); + }; + + const begin = () => { + debug('run(): emitting %s', constants.EVENT_RUN_BEGIN); + this.emit(constants.EVENT_RUN_BEGIN); + debug('run(): emitted %s', constants.EVENT_RUN_BEGIN); + + this.runSuite(rootSuite, async () => { + end(); + }); + }; + + const prepare = () => { debug('run(): starting'); // If there is an `only` filter if (rootSuite.hasOnly()) { rootSuite.filterOnly(); debug('run(): filtered exclusive Runnables'); } - self.state = constants.STATE_RUNNING; - if (self._delay) { - self.emit(constants.EVENT_DELAY_END); + this.state = constants.STATE_RUNNING; + if (this._delay) { + this.emit(constants.EVENT_DELAY_END); debug('run(): "delay" ended'); } - debug('run(): emitting %s', constants.EVENT_RUN_BEGIN); - self.emit(constants.EVENT_RUN_BEGIN); - debug('run(): emitted %s', constants.EVENT_RUN_BEGIN); - self.runSuite(rootSuite, function() { - debug( - 'run(): root suite completed; emitting %s', - constants.EVENT_RUN_END - ); - self.emit(constants.EVENT_RUN_END); - debug('run(): emitted %s', constants.EVENT_RUN_END); - }); - } + return begin(); + }; // references cleanup to avoid memory leaks if (this._opts.cleanReferencesAfterRun) { - this.on(constants.EVENT_SUITE_END, function(suite) { + this.on(constants.EVENT_SUITE_END, suite => { suite.cleanReferences(); }); } // callback this.on(constants.EVENT_RUN_END, function() { - self.state = constants.STATE_STOPPED; - debug(constants.EVENT_RUN_END); + this.state = constants.STATE_STOPPED; debug('run(): emitted %s', constants.EVENT_RUN_END); - fn(self.failures); + fn(this.failures); }); - self._removeEventListener(process, 'uncaughtException', self.uncaught); - self._addEventListener(process, 'uncaughtException', self.uncaught); + this._removeEventListener(process, 'uncaughtException', this.uncaught); + this._removeEventListener(process, 'unhandledRejection', this.uncaught); + this._addEventListener(process, 'uncaughtException', this.uncaught); + this._addEventListener(process, 'unhandledRejection', this.uncaught); if (this._delay) { // for reporters, I guess. // might be nice to debounce some dots while we wait. this.emit(constants.EVENT_DELAY_BEGIN, rootSuite); - rootSuite.once(EVENT_ROOT_SUITE_RUN, start); + rootSuite.once(EVENT_ROOT_SUITE_RUN, prepare); debug('run(): waiting for green light due to --delay'); } else { - Runner.immediately(function() { - start(); - }); + Runner.immediately(prepare); } return this; diff --git a/lib/utils.js b/lib/utils.js index 64dfe9b964..f5dded3760 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -645,3 +645,21 @@ exports.cwd = function cwd() { exports.isBrowser = function isBrowser() { return Boolean(process.browser); }; + +/** + * Casts `value` to an array; useful for optionally accepting array parameters + * + * It follows these rules, depending on `value`. If `value` is... + * 1. An `Array`, return a copy + * 2. An `arguments` object, return a copy + * 3. Any defined value, return a new array containing the single value + * 4. `undefined`, return an empty array + * @param {*} value - Something to cast to an array + * @returns {Array<*>} + */ +exports.castArray = function castArray(value) { + if (Array.isArray(value) || type(value) === 'arguments') { + return Array.prototype.slice.call(value); + } + return typeof value !== 'undefined' ? [].concat(value) : []; +}; diff --git a/package-lock.json b/package-lock.json index f8b8be6604..62eb2fa9a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -352,15 +352,15 @@ } }, "caniuse-lite": { - "version": "1.0.30001115", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001115.tgz", - "integrity": "sha512-NZrG0439ePYna44lJX8evHX2L7Z3/z3qjVLnHgbBb/duNEnGo348u+BQS5o4HTWcrb++100dHFrU36IesIrC1Q==", + "version": "1.0.30001116", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz", + "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==", "dev": true }, "electron-to-chromium": { - "version": "1.3.535", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.535.tgz", - "integrity": "sha512-5k7WGdl1ZnbcU97acUnY/UXu6bCMDnKCAnEc1N0xNToPvMCp99PEvh5K3xNr4ZUVCf2FuratM++NgOxCtbtXzA==", + "version": "1.3.536", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.536.tgz", + "integrity": "sha512-aU16nvH8/zNNeFIQ7H2SKRQlJ/srw7mCn/JDj2ImWUA7Lk2+3zJFpDGJNP2qRxPAZsC+qgnlgNTYIvT6EOdJFQ==", "dev": true }, "node-releases": { @@ -512,15 +512,6 @@ "to-fast-properties": "^2.0.0" } }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", @@ -669,15 +660,15 @@ } }, "caniuse-lite": { - "version": "1.0.30001115", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001115.tgz", - "integrity": "sha512-NZrG0439ePYna44lJX8evHX2L7Z3/z3qjVLnHgbBb/duNEnGo348u+BQS5o4HTWcrb++100dHFrU36IesIrC1Q==", + "version": "1.0.30001116", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz", + "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==", "dev": true }, "electron-to-chromium": { - "version": "1.3.535", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.535.tgz", - "integrity": "sha512-5k7WGdl1ZnbcU97acUnY/UXu6bCMDnKCAnEc1N0xNToPvMCp99PEvh5K3xNr4ZUVCf2FuratM++NgOxCtbtXzA==", + "version": "1.3.536", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.536.tgz", + "integrity": "sha512-aU16nvH8/zNNeFIQ7H2SKRQlJ/srw7mCn/JDj2ImWUA7Lk2+3zJFpDGJNP2qRxPAZsC+qgnlgNTYIvT6EOdJFQ==", "dev": true }, "node-releases": { @@ -2930,15 +2921,15 @@ } }, "caniuse-lite": { - "version": "1.0.30001115", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001115.tgz", - "integrity": "sha512-NZrG0439ePYna44lJX8evHX2L7Z3/z3qjVLnHgbBb/duNEnGo348u+BQS5o4HTWcrb++100dHFrU36IesIrC1Q==", + "version": "1.0.30001116", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz", + "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==", "dev": true }, "electron-to-chromium": { - "version": "1.3.535", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.535.tgz", - "integrity": "sha512-5k7WGdl1ZnbcU97acUnY/UXu6bCMDnKCAnEc1N0xNToPvMCp99PEvh5K3xNr4ZUVCf2FuratM++NgOxCtbtXzA==", + "version": "1.3.536", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.536.tgz", + "integrity": "sha512-aU16nvH8/zNNeFIQ7H2SKRQlJ/srw7mCn/JDj2ImWUA7Lk2+3zJFpDGJNP2qRxPAZsC+qgnlgNTYIvT6EOdJFQ==", "dev": true }, "lodash": { @@ -3520,16 +3511,6 @@ "@wdio/logger": "6.0.16" } }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -3604,31 +3585,6 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "acorn-walk": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", - "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", - "dev": true - } - } - }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -4488,15 +4444,15 @@ } }, "caniuse-lite": { - "version": "1.0.30001115", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001115.tgz", - "integrity": "sha512-NZrG0439ePYna44lJX8evHX2L7Z3/z3qjVLnHgbBb/duNEnGo348u+BQS5o4HTWcrb++100dHFrU36IesIrC1Q==", + "version": "1.0.30001116", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz", + "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==", "dev": true }, "electron-to-chromium": { - "version": "1.3.535", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.535.tgz", - "integrity": "sha512-5k7WGdl1ZnbcU97acUnY/UXu6bCMDnKCAnEc1N0xNToPvMCp99PEvh5K3xNr4ZUVCf2FuratM++NgOxCtbtXzA==", + "version": "1.3.536", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.536.tgz", + "integrity": "sha512-aU16nvH8/zNNeFIQ7H2SKRQlJ/srw7mCn/JDj2ImWUA7Lk2+3zJFpDGJNP2qRxPAZsC+qgnlgNTYIvT6EOdJFQ==", "dev": true }, "node-releases": { @@ -4745,6 +4701,43 @@ "execa": "^0.7.0", "p-map-series": "^1.0.0", "tempfile": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "bin-check": { @@ -4755,6 +4748,43 @@ "requires": { "execa": "^0.7.0", "executable": "^4.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "bin-version": { @@ -5168,55 +5198,12 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, - "browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -6210,90 +6197,6 @@ "stream-throttle": "^0.1.3" } }, - "browserify": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.1.tgz", - "integrity": "sha512-EQX0h59Pp+0GtSRb5rL6OTfrttlzv+uyaUVlK6GX3w11SQ0jKPKyjC/54RhPR2ib2KmfcELM06e8FxcI5XNU2A==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^1.11.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.0", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^2.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.0.0", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.0.0", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "~0.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^2.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.10.1", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -6546,12 +6449,6 @@ } } }, - "cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", - "dev": true - }, "caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -7292,26 +7189,6 @@ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, - "combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", - "dev": true, - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - }, - "dependencies": { - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", - "dev": true - } - } - }, "combine-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/combine-stream/-/combine-stream-0.0.4.tgz", @@ -7720,10 +7597,13 @@ "dev": true }, "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", - "dev": true + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } }, "cookie": { "version": "0.3.1", @@ -8174,12 +8054,6 @@ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", "dev": true }, - "dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", - "dev": true - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -8541,30 +8415,6 @@ "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==", "dev": true }, - "deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -8615,17 +8465,6 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, - "detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "dev": true, - "requires": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - } - }, "dev-ip": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", @@ -8909,15 +8748,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -9888,9 +9718,9 @@ "dev": true }, "events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", "dev": true }, "evp_bytestokey": { @@ -10284,12 +10114,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", - "dev": true - }, "fastq": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", @@ -10802,12 +10626,6 @@ "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", "dev": true }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -11671,12 +11489,6 @@ "uglify-js": "^3.5.1" } }, - "htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", - "dev": true - }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", @@ -12261,15 +12073,6 @@ } } }, - "inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "dev": true, - "requires": { - "source-map": "~0.5.3" - } - }, "inquirer": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", @@ -12377,42 +12180,6 @@ } } }, - "insert-module-globals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", - "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", @@ -13387,15 +13154,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13432,18 +13190,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -13931,16 +13677,6 @@ "graceful-fs": "^4.1.9" } }, - "labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -15931,41 +15667,6 @@ "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==", "dev": true }, - "module-deps": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.2.tgz", - "integrity": "sha512-a9y6yDv5u5I4A+IPHTnqFxcaKr4p50/zxTjcQJaX2ws9tN/W6J6YXnEKhqRyPhl494dkcxx951onSKVezmI+3w==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "browser-resolve": "^1.7.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -16166,12 +15867,6 @@ "isarray": "^1.0.0" } }, - "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", - "dev": true - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -16183,51 +15878,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } } } }, @@ -16700,15 +16350,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -17147,15 +16788,6 @@ "os-tmpdir": "^1.0.0" } }, - "outpipe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", - "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", - "dev": true, - "requires": { - "shell-quote": "^1.4.2" - } - }, "p-cancelable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", @@ -17311,15 +16943,6 @@ } } }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "dev": true, - "requires": { - "path-platform": "~0.11.15" - } - }, "parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", @@ -17503,12 +17126,6 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", - "dev": true - }, "path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", @@ -19180,15 +18797,6 @@ "gather-stream": "^1.0.0" } }, - "read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -20883,25 +20491,6 @@ } } }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "dev": true, - "requires": { - "json-stable-stringify": "~0.0.0", - "sha.js": "~2.4.4" - } - }, - "shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "dev": true, - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -20917,12 +20506,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, "sift": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", @@ -21675,16 +21258,6 @@ "duplexer": "~0.1.1" } }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, "stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -21692,38 +21265,16 @@ "dev": true }, "stream-http": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.0.tgz", - "integrity": "sha512-cuB6RgO7BqC4FBYzmnvhob5Do3wIdIsXAgGycHJnW+981gHqoYcYz9lqjJrk8WXRddbwPuqPYRl+bag6mYv4lw==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dev": true, "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", - "readable-stream": "^3.0.6", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" } }, "stream-throttle": { @@ -22020,15 +21571,6 @@ } } }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "requires": { - "minimist": "^1.1.0" - } - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -22154,15 +21696,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "dev": true, - "requires": { - "acorn-node": "^1.2.0" - } - }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -22543,12 +22076,12 @@ "dev": true }, "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", "dev": true, "requires": { - "process": "~0.11.0" + "setimmediate": "^1.0.4" } }, "timsort": { @@ -22677,6 +22210,26 @@ "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==", "dev": true }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -22765,9 +22318,9 @@ "dev": true }, "tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, "tunnel": { @@ -22886,12 +22439,6 @@ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, - "umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", - "dev": true - }, "unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -22908,19 +22455,6 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, - "undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", - "dev": true, - "requires": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - } - }, "underscore": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", @@ -23690,9 +23224,9 @@ } }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -23842,817 +23376,6 @@ "xml-name-validator": "^3.0.0" } }, - "watchify": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.1.tgz", - "integrity": "sha512-WwnUClyFNRMB2NIiHgJU9RQPQNqVeFk7OmZaWf5dC5EnNa0Mgr7imBydbaJ7tGTuPM2hz1Cb4uiBvK9NVxMfog==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "browserify": "^16.1.0", - "chokidar": "^2.1.1", - "defined": "^1.0.0", - "outpipe": "^1.1.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - } - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": false, - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "resolved": false, - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": false, - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": false, - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": false, - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": false, - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": false, - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "resolved": false, - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": false, - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "resolved": false, - "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "resolved": false, - "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==", - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "resolved": false, - "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "resolved": false, - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": false, - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "resolved": false, - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": false, - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": false, - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": false, - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": false, - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "resolved": false, - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "resolved": false, - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": false, - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "resolved": false, - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, "webdriver": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-6.4.0.tgz", diff --git a/package.json b/package.json index 963a670be0..159b01cf67 100644 --- a/package.json +++ b/package.json @@ -144,13 +144,13 @@ "svgo": "^1.3.2", "through2": "^4.0.2", "to-vfile": "^6.1.0", + "touch": "^3.1.0", "unexpected": "^11.14.0", "unexpected-eventemitter": "^2.2.0", "unexpected-sinon": "^10.11.2", "update-notifier": "^4.1.0", "uslug": "^1.0.4", - "uuid": "^8.3.0", - "watchify": "^3.11.1" + "uuid": "^8.3.0" }, "files": [ "bin/*mocha", diff --git a/test/assertions.js b/test/assertions.js index ef678ff4ea..030e8dbb2e 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -1,5 +1,7 @@ 'use strict'; +const escapeRe = require('escape-string-regexp'); + exports.mixinMochaAssertions = function(expect) { return expect .addType({ @@ -7,7 +9,8 @@ exports.mixinMochaAssertions = function(expect) { base: 'object', identify: function(v) { return ( - Object.prototype.toString.call(v) === '[object Object]' && + v !== null && + typeof v === 'object' && typeof v.output === 'string' && 'code' in v && // may be null Array.isArray(v.args) @@ -15,19 +18,22 @@ exports.mixinMochaAssertions = function(expect) { } }) .addType({ - name: 'JSONRunResult', + name: 'JSONResult', base: 'object', identify: function(v) { return ( - Object.prototype.toString.call(v) === '[object Object]' && - Object.prototype.toString.call(v.stats) === '[object Object]' && + v !== null && + typeof v === 'object' && + v.stats !== null && + typeof v.stats === 'object' && Array.isArray(v.failures) && - typeof v.code === 'number' + typeof v.code === 'number' && + typeof v.command === 'string' ); } }) .addType({ - name: 'RawRunResult', + name: 'SummarizedResult', base: 'object', identify: function(v) { return ( @@ -40,7 +46,7 @@ exports.mixinMochaAssertions = function(expect) { ); } }) - .addAssertion(' [not] to have (passed|succeeded)', function( + .addAssertion(' [not] to have (passed|succeeded)', function( expect, result ) { @@ -53,19 +59,19 @@ exports.mixinMochaAssertions = function(expect) { }); }) .addAssertion( - ' [not] to have (passed|succeeded)', + ' [not] to have (passed|succeeded)', function(expect, result) { expect(result, '[not] to have property', 'code', 0); } ) .addAssertion( - ' [not] to have completed with [exit] code ', + ' [not] to have completed with [exit] code ', function(expect, result, code) { expect(result.code, '[not] to be', code); } ) .addAssertion( - ' [not] to have passed (with|having) count ', + ' [not] to have passed (with|having) count ', function(expect, result, count) { expect(result, '[not] to pass').and('[not] to satisfy', { stats: {passes: expect.it('to be', count)} @@ -73,14 +79,14 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have failed (with|having) count ', + ' [not] to have failed (with|having) count ', function(expect, result, count) { expect(result, '[not] to have failed').and('[not] to satisfy', { stats: {failures: expect.it('to be', count)} }); } ) - .addAssertion(' [not] to have failed', function( + .addAssertion(' [not] to have failed', function( expect, result ) { @@ -92,7 +98,7 @@ exports.mixinMochaAssertions = function(expect) { failures: expect.it('to be non-empty') }); }) - .addAssertion(' [not] to have failed', function( + .addAssertion(' [not] to have failed', function( expect, result ) { @@ -101,7 +107,7 @@ exports.mixinMochaAssertions = function(expect) { }); }) .addAssertion( - ' [not] to have failed (with|having) output ', + ' [not] to have failed (with|having) output ', function(expect, result, output) { expect(result, '[not] to satisfy', { code: expect.it('to be greater than', 0), @@ -110,7 +116,7 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have passed (with|having) output ', + ' [not] to have passed (with|having) output ', function(expect, result, output) { expect(result, '[not] to satisfy', { code: 0, @@ -119,24 +125,24 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have failed [test] count ', + ' [not] to have failed [test] count ', function(expect, result, count) { expect(result.failing, '[not] to be', count); } ) .addAssertion( - ' [not] to have passed [test] count ', + ' [not] to have passed [test] count ', function(expect, result, count) { expect(result.passing, '[not] to be', count); } ) .addAssertion( - ' [not] to have pending [test] count ', + ' [not] to have pending [test] count ', function(expect, result, count) { expect(result.pending, '[not] to be', count); } ) - .addAssertion(' [not] to have test count ', function( + .addAssertion(' [not] to have test count ', function( expect, result, count @@ -144,25 +150,25 @@ exports.mixinMochaAssertions = function(expect) { expect(result.stats.tests, '[not] to be', count); }) .addAssertion( - ' [not] to have failed [test] count ', + ' [not] to have failed [test] count ', function(expect, result, count) { expect(result.stats.failures, '[not] to be', count); } ) .addAssertion( - ' [not] to have passed [test] count ', + ' [not] to have passed [test] count ', function(expect, result, count) { expect(result.stats.passes, '[not] to be', count); } ) .addAssertion( - ' [not] to have pending [test] count ', + ' [not] to have pending [test] count ', function(expect, result, count) { expect(result.stats.pending, '[not] to be', count); } ) .addAssertion( - ' [not] to have run (test|tests) ', + ' [not] to have run (test|tests) ', function(expect, result, titles) { Array.prototype.slice.call(arguments, 2).forEach(function(title) { expect( @@ -174,7 +180,7 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have failed (test|tests) ', + ' [not] to have failed (test|tests) ', function(expect, result, titles) { Array.prototype.slice.call(arguments, 2).forEach(function(title) { expect(result.failures, '[not] to have an item satisfying', { @@ -184,7 +190,7 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have failed with (error|errors) ', + ' [not] to have failed with (error|errors) ', function(expect, result, errors) { Array.prototype.slice.call(arguments, 2).forEach(function(error) { expect(result, '[not] to have failed').and('[not] to satisfy', { @@ -197,22 +203,23 @@ exports.mixinMochaAssertions = function(expect) { }); } ) - .addAssertion( - ' [not] to have (error|errors) ', - function(expect, result, errors) { - Array.prototype.slice.call(arguments, 2).forEach(function(error) { - expect(result, '[not] to satisfy', { - failures: expect.it('to have an item satisfying', { - err: expect - .it('to satisfy', error) - .or('to satisfy', {message: error}) - }) - }); + .addAssertion(' [not] to have (error|errors) ', function( + expect, + result, + errors + ) { + Array.prototype.slice.call(arguments, 2).forEach(function(error) { + expect(result, '[not] to satisfy', { + failures: expect.it('to have an item satisfying', { + err: expect + .it('to satisfy', error) + .or('to satisfy', {message: error}) + }) }); - } - ) + }); + }) .addAssertion( - ' [not] to have passed (test|tests) ', + ' [not] to have passed (test|tests) ', function(expect, result, titles) { Array.prototype.slice.call(arguments, 2).forEach(function(title) { expect(result.passes, '[not] to have an item satisfying', { @@ -222,7 +229,7 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have test order ', + ' [not] to have test order ', function(expect, result, state, titles) { expect( result[state].slice(0, titles.length), @@ -234,13 +241,13 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have passed test order ', + ' [not] to have passed test order ', function(expect, result, titles) { expect(result, '[not] to have test order', 'passes', titles); } ) .addAssertion( - ' [not] to have passed test order ', + ' [not] to have passed test order ', function(expect, result, titles) { expect( result, @@ -251,13 +258,13 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have failed test order ', + ' [not] to have failed test order ', function(expect, result, titles) { expect(result, '[not] to have test order', 'failures', titles); } ) .addAssertion( - ' [not] to have failed test order ', + ' [not] to have failed test order ', function(expect, result, titles) { expect( result, @@ -268,13 +275,13 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to have pending test order ', + ' [not] to have pending test order ', function(expect, result, titles) { expect(result, '[not] to have test order', 'pending', titles); } ) .addAssertion( - ' [not] to have pending test order ', + ' [not] to have pending test order ', function(expect, result, titles) { expect( result, @@ -284,41 +291,39 @@ exports.mixinMochaAssertions = function(expect) { ); } ) - .addAssertion(' [not] to have pending tests', function( + .addAssertion(' [not] to have pending tests', function( expect, result ) { expect(result.stats.pending, '[not] to be greater than', 0); }) - .addAssertion(' [not] to have passed tests', function( + .addAssertion(' [not] to have passed tests', function( expect, result ) { expect(result.stats.passes, '[not] to be greater than', 0); }) - .addAssertion(' [not] to have failed tests', function( + .addAssertion(' [not] to have failed tests', function( expect, result ) { expect(result.stats.failed, '[not] to be greater than', 0); }) - .addAssertion(' [not] to have tests', function( + .addAssertion(' [not] to have tests', function(expect, result) { + expect(result.stats.tests, '[not] to be greater than', 0); + }) + .addAssertion(' [not] to have retried test ', function( expect, - result + result, + title ) { - expect(result.stats.tests, '[not] to be greater than', 0); + expect(result.tests, '[not] to have an item satisfying', { + title: title, + currentRetry: expect.it('to be positive') + }); }) .addAssertion( - ' [not] to have retried test ', - function(expect, result, title) { - expect(result.tests, '[not] to have an item satisfying', { - title: title, - currentRetry: expect.it('to be positive') - }); - } - ) - .addAssertion( - ' [not] to have retried test ', + ' [not] to have retried test ', function(expect, result, title, count) { expect(result.tests, '[not] to have an item satisfying', { title: title, @@ -327,13 +332,25 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' [not] to contain [output] ', + ' [not] to contain [output] ', function(expect, result, output) { expect(result.output, '[not] to satisfy', output); } ) .addAssertion( - ' to have [exit] code ', + ' to contain [output] once ', + function(expect, result, output) { + if (typeof output === 'string') { + output = escapeRe(output); + } else if (!(output instanceof RegExp)) { + throw new TypeError('expected a string or regexp'); + } + output = new RegExp(output, 'g'); + expect(result.output.match(output), 'to have length', 1); + } + ) + .addAssertion( + ' to have [exit] code ', function(expect, result, code) { expect(result.code, 'to be', code); } diff --git a/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js b/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js new file mode 100644 index 0000000000..9ae6805e77 --- /dev/null +++ b/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js @@ -0,0 +1,20 @@ +'use strict'; + +exports.mochaGlobalSetup = [ + async function() { + this.foo = 0; + }, + function() { + this.foo = this.foo + 1; + } +]; + +exports.mochaGlobalTeardown = [ + async function() { + this.foo = this.foo + 1; + }, + function() { + this.foo = this.foo + 1; + console.log(`teardown: this.foo = ${this.foo}`); + } +]; diff --git a/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js b/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js new file mode 100644 index 0000000000..aec78c443f --- /dev/null +++ b/test/integration/fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js @@ -0,0 +1,10 @@ +'use strict'; + +exports.mochaGlobalSetup = async function() { + this.foo = 'bar'; + console.log(`setup: this.foo = ${this.foo}`); +}; + +exports.mochaGlobalTeardown = async function() { + console.log(`teardown: this.foo = ${this.foo}`); +}; diff --git a/test/integration/fixtures/options/require/esm/package.json b/test/integration/fixtures/plugins/root-hooks/esm/package.json similarity index 100% rename from test/integration/fixtures/options/require/esm/package.json rename to test/integration/fixtures/plugins/root-hooks/esm/package.json diff --git a/test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js b/test/integration/fixtures/plugins/root-hooks/esm/root-hook-defs-esm.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js rename to test/integration/fixtures/plugins/root-hooks/esm/root-hook-defs-esm.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-a.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-a.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-b.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-b.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-b.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-b.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-c.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-c.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-c.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-c.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-d.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-d.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-d.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-d.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-esm-broken.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-esm-broken.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-esm-broken.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-esm-broken.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs b/test/integration/fixtures/plugins/root-hooks/root-hook-defs-esm.fixture.mjs similarity index 100% rename from test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs rename to test/integration/fixtures/plugins/root-hooks/root-hook-defs-esm.fixture.mjs diff --git a/test/integration/fixtures/options/require/root-hook-test-2.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-test-2.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-test-2.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-test-2.fixture.js diff --git a/test/integration/fixtures/options/require/root-hook-test.fixture.js b/test/integration/fixtures/plugins/root-hooks/root-hook-test.fixture.js similarity index 100% rename from test/integration/fixtures/options/require/root-hook-test.fixture.js rename to test/integration/fixtures/plugins/root-hooks/root-hook-test.fixture.js diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 6475262443..52f6330438 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -1,71 +1,34 @@ 'use strict'; -var format = require('util').format; -var spawn = require('cross-spawn').spawn; -var path = require('path'); -var Base = require('../../lib/reporters/base'); -var debug = require('debug')('mocha:tests:integration:helpers'); -var DEFAULT_FIXTURE = resolveFixturePath('__default__'); -var MOCHA_EXECUTABLE = require.resolve('../../bin/mocha'); -var _MOCHA_EXECUTABLE = require.resolve('../../bin/_mocha'); +const escapeRegExp = require('escape-string-regexp'); +const {sync: rimraf} = require('rimraf'); +const os = require('os'); +const fs = require('fs-extra'); +const {format} = require('util'); +const path = require('path'); +const Base = require('../../lib/reporters/base'); +const debug = require('debug')('mocha:tests:integration:helpers'); +const touch = require('touch'); -module.exports = { - DEFAULT_FIXTURE: DEFAULT_FIXTURE, - - /** - * regular expression used for splitting lines based on new line / dot symbol. - */ - splitRegExp: new RegExp('[\\n' + Base.symbols.dot + ']+'), - - /** - * Invokes the mocha binary. Accepts an array of additional command line args - * to pass. The callback is invoked with the exit code and output. Optional - * current working directory as final parameter. - * - * By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you - * want it. - * - * In most cases runMocha should be used instead. - * - * Example response: - * { - * code: 1, - * output: '...' - * } - * - * @param {Array} args - Extra args to mocha executable - * @param {Function} done - Callback - * @param {Object} [opts] - Options for `spawn()` - */ - invokeMocha: invokeMocha, - - invokeMochaAsync: invokeMochaAsync, - - invokeNode: invokeNode, - - getSummary: getSummary, +/** + * Path to `mocha` executable + */ +const MOCHA_EXECUTABLE = require.resolve('../../bin/mocha'); - /** - * Resolves the path to a fixture to the full path. - */ - resolveFixturePath: resolveFixturePath, +/** + * regular expression used for splitting lines based on new line / dot symbol. + */ +const SPLIT_DOT_REPORTER_REGEXP = new RegExp('[\\n' + Base.symbols.dot + ']+'); - toJSONRunResult: toJSONRunResult, +/** + * Name of "default" fixture file. + */ +const DEFAULT_FIXTURE = '__default__'; - /** - * Given a regexp-like string, escape it so it can be used with the `RegExp` constructor - * @param {string} str - string to be escaped - * @returns {string} Escaped string - */ - escapeRegExp: function escapeRegExp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - }, - - runMocha: runMocha, - runMochaJSON: runMochaJSON, - runMochaAsync: runMochaAsync, - runMochaJSONAsync: runMochaJSONAsync -}; +/** + * Path to "default" fixture file + */ +const DEFAULT_FIXTURE_PATH = resolveFixturePath(DEFAULT_FIXTURE); /** * Invokes the mocha binary for the given fixture with color output disabled. @@ -86,76 +49,65 @@ module.exports = { * } * * @param {string} fixturePath - Path to fixture .js file - * @param {string[]} args - Extra args to mocha executable - * @param {Function} fn - Callback + * @param {string[]|SummarizedResultCallback} args - Extra args to mocha executable + * @param {SummarizedResultCallback|Object} done - Callback * @param {Object} [opts] - Options for `spawn()` - * @returns {ChildProcess} Mocha process + * @returns {ChildProcess} Subprocess process */ -function runMocha(fixturePath, args, fn, opts) { +function runMocha(fixturePath, args, done, opts = {}) { if (typeof args === 'function') { - opts = fn; - fn = args; + opts = done; + done = args; args = []; } - var path; - - path = resolveFixturePath(fixturePath); - args = args || []; - - return invokeSubMocha( - args.concat(path), - function(err, res) { + return invokeMocha( + [...args, resolveFixturePath(fixturePath)], + (err, res) => { if (err) { - return fn(err); + return done(err); } - fn(null, getSummary(res)); + done(null, getSummary(res)); }, opts ); } /** - * Invokes the mocha binary for the given fixture using the JSON reporter, - * returning the parsed output, as well as exit code. + * Invokes the mocha executable for the given fixture using the `json` reporter, + * calling callback `done` with parsed output. + * + * Use when you expect `mocha` _not_ to fail (test failures OK); the output from + * the `json` reporter--and thus the entire subprocess--must be valid JSON! * * By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you * want it. - * @param {string} fixturePath - Path from __dirname__ - * @param {string[]} args - Array of args - * @param {Function} fn - Callback + * @param {string} fixturePath - Path from `__dirname__` + * @param {string[]|JSONResultCallback} args - Args to `mocha` or callback + * @param {JSONResultCallback|Object} done - Callback or options * @param {Object} [opts] - Opts for `spawn()` - * @returns {*} Parsed object + * @returns {ChildProcess} Subprocess instance */ -function runMochaJSON(fixturePath, args, fn, opts) { +function runMochaJSON(fixturePath, args, done, opts) { if (typeof args === 'function') { - opts = fn; - fn = args; + opts = done; + done = args; args = []; } - var path; - - path = resolveFixturePath(fixturePath); - args = (args || []).concat('--reporter', 'json', path); - return invokeMocha( - args, - function(err, res) { + [...args, '--reporter', 'json', resolveFixturePath(fixturePath)], + (err, res) => { if (err) { - return fn(err); + return done(err); } - var result; + let result; try { - // attempt to catch a JSON parsing error *only* here. - // previously, the callback was called within this `try` block, - // which would result in errors thrown from the callback - // getting caught by the `catch` block below. - result = toJSONRunResult(res); + result = toJSONResult(res); } catch (err) { - return fn( + return done( new Error( format( 'Failed to parse JSON reporter output. Error:\n%O\nResult:\n%O', @@ -165,7 +117,7 @@ function runMochaJSON(fixturePath, args, fn, opts) { ) ); } - fn(null, result); + done(null, result); }, opts ); @@ -202,6 +154,7 @@ function runMochaAsync(fixturePath, args, opts) { * @param {string} fixturePath - Path to (or name of, or basename of) fixture file * @param {Options} [args] - Command-line args * @param {Object} [opts] - Options for `child_process.spawn` + * @returns {Promise} */ function runMochaJSONAsync(fixturePath, args, opts) { return new Promise(function(resolve, reject) { @@ -220,17 +173,15 @@ function runMochaJSONAsync(fixturePath, args, opts) { } /** - * Coerce output as returned by _spawnMochaWithListeners using JSON reporter into a JSONRunResult as + * Coerce output as returned by _spawnMochaWithListeners using JSON reporter into a JSONResult as * recognized by our custom unexpected assertions - * @param {string} result - Raw stdout from Mocha run using JSON reporter - * @private + * @param {RawResult} result - Raw stdout from Mocha run using JSON reporter + * @returns {JSONResult} */ -function toJSONRunResult(result) { - var code = result.code; +function toJSONResult(result) { + const {code, command, output} = result; try { - result = JSON.parse(result.output); - result.code = code; - return result; + return {...JSON.parse(output), code, command}; } catch (err) { throw new Error( `Couldn't parse JSON: ${err.message}\n\nOriginal result output: ${result.output}` @@ -251,12 +202,13 @@ function toJSONRunResult(result) { * - The {@link DEFAULT_FIXTURE} file is used if no arguments are provided. * * @param {string[]|*} [args] - Arguments to `spawn` - * @returns string[] + * @returns {string[]} */ -function defaultArgs(args) { - var newArgs = (!args || !args.length ? [DEFAULT_FIXTURE] : args).concat([ +function defaultArgs(args = [DEFAULT_FIXTURE_PATH]) { + const newArgs = [ + ...(!args.length ? [DEFAULT_FIXTURE_PATH] : args), '--no-color' - ]); + ]; if (!newArgs.some(arg => /--(no-)?bail/.test(arg))) { newArgs.push('--no-bail'); } @@ -266,15 +218,26 @@ function defaultArgs(args) { return newArgs; } -function invokeMocha(args, fn, opts) { +/** + * Invoke `mocha` with default arguments. Calls `done` upon exit. Does _not_ accept a fixture path. + * + * Good for testing error conditions. This is low-level, and you likely want + * {@link runMocha} or even {@link runMochaJSON} if you are running test fixtures. + * + * @param {string[]|RawResultCallback} args - Args to `mocha` or callback + * @param {RawResultCallback|Object} done - Callback or options + * @param {Object} [opts] - Options + * @returns {ChildProcess} + */ +function invokeMocha(args, done, opts = {}) { if (typeof args === 'function') { - opts = fn; - fn = args; + opts = done; + done = args; args = []; } - return _spawnMochaWithListeners( + return createSubprocess( defaultArgs([MOCHA_EXECUTABLE].concat(args)), - fn, + done, opts ); } @@ -290,12 +253,12 @@ function invokeMocha(args, fn, opts) { * * @param {string[]} args - Array of args * @param {Object} [opts] - Opts for `spawn()` - * @returns {[ChildProcess|Promise]} + * @returns {[import('child_process').ChildProcess,Promise]} A tuple of process and result promise */ -function invokeMochaAsync(args, opts) { +function invokeMochaAsync(args, opts = {}) { let mochaProcess; const resultPromise = new Promise((resolve, reject) => { - mochaProcess = _spawnMochaWithListeners( + mochaProcess = createSubprocess( defaultArgs([MOCHA_EXECUTABLE].concat(args)), (err, result) => { if (err) { @@ -311,62 +274,74 @@ function invokeMochaAsync(args, opts) { } /** - * Invokes Node without Mocha binary with the given arguments, - * when Mocha is used programmatically. + * Invokes subprocess with currently-running `node`. + * + * Useful for running certain fixtures as scripts. + * + * @param {string[]|RawResultCallback} args - Args to `mocha` or callback + * @param {RawResultCallback|Object} done - Callback or options + * @param {Object} [opts] - Options + * @returns {ChildProcess} */ -function invokeNode(args, fn, opts) { +function invokeNode(args, done, opts = {}) { if (typeof args === 'function') { - opts = fn; - fn = args; + opts = done; + done = args; args = []; } - return _spawnMochaWithListeners(args, fn, opts); -} - -function invokeSubMocha(args, fn, opts) { - if (typeof args === 'function') { - opts = fn; - fn = args; - args = []; - } - return _spawnMochaWithListeners( - defaultArgs([_MOCHA_EXECUTABLE].concat(args)), - fn, - opts - ); + return createSubprocess(args, done, opts); } /** - * Spawns Mocha in a subprocess and returns an object containing its output and exit code + * Creates a subprocess and calls callback `done` when it has exited. + * + * This is the most low-level function and should _not_ be exported. * * @param {string[]} args - Path to executable and arguments - * @param {Function} fn - Callback - * @param {Object|string} [opts] - Options to `cross-spawn`, or 'pipe' for shortcut to `{stdio: pipe}` - * @returns {ChildProcess} - * @private + * @param {RawResultCallback} done - Callback + * @param {Object|string} [opts] - Options to `cross-spawn` or `child_process.fork` or 'pipe' for shortcut to `{stdio: pipe}` + * @param {boolean} [opts.fork] - If `true`, use `child_process.fork` instead + * @returns {import('child_process').ChildProcess} */ -function _spawnMochaWithListeners(args, fn, opts) { - var output = ''; - opts = opts || {}; +function createSubprocess(args, done, opts = {}) { + let output = ''; + if (opts === 'pipe') { opts = {stdio: ['inherit', 'pipe', 'pipe']}; } - var env = Object.assign({}, process.env); + + const env = {...process.env}; // prevent DEBUG from borking STDERR when piping, unless explicitly set via `opts` delete env.DEBUG; - opts = Object.assign( - { - cwd: process.cwd(), - stdio: ['inherit', 'pipe', 'inherit'], - env: env - }, - opts - ); + opts = { + cwd: process.cwd(), + stdio: ['inherit', 'pipe', 'inherit'], + env, + ...opts + }; - debug('spawning: %s', [process.execPath].concat(args).join(' ')); - var mocha = spawn(process.execPath, args, opts); - var listener = function(data) { + /** + * @type {import('child_process').ChildProcess} + */ + let mocha; + if (opts.fork) { + const {fork} = require('child_process'); + // to use ipc, we need a fourth item in `stdio` array. + // opts.stdio is usually an array of length 3, but it could be smaller + // (pad with `null`) + for (let i = opts.stdio.length; i < 4; i++) { + opts.stdio.push(i === 3 ? 'ipc' : null); + } + debug('forking: %s', args.join(' ')); + mocha = fork(args[0], args.slice(1), opts); + } else { + const {spawn} = require('cross-spawn'); + debug('spawning: %s', [process.execPath].concat(args).join(' ')); + mocha = spawn(process.execPath, args, opts); + } + + const listener = data => { output += data; }; @@ -374,13 +349,13 @@ function _spawnMochaWithListeners(args, fn, opts) { if (mocha.stderr) { mocha.stderr.on('data', listener); } - mocha.on('error', fn); + mocha.on('error', done); - mocha.on('close', function(code) { - fn(null, { - output: output, - code: code, - args: args, + mocha.on('close', code => { + done(null, { + output, + code, + args, command: args.join(' ') }); }); @@ -388,13 +363,19 @@ function _spawnMochaWithListeners(args, fn, opts) { return mocha; } +/** + * Given a fixture "name" (a relative path from `${__dirname}/fixtures`), + * with or without extension, or an absolute path, resolve a fixture filepath + * @param {string} fixture - Fixture name + * @returns {string} Resolved filepath + */ function resolveFixturePath(fixture) { if (path.extname(fixture) !== '.js' && path.extname(fixture) !== '.mjs') { fixture += '.fixture.js'; } return path.isAbsolute(fixture) ? fixture - : path.join('test', 'integration', 'fixtures', fixture); + : path.resolve(__dirname, 'fixtures', fixture); } /** @@ -414,6 +395,171 @@ function getSummary(res) { }, res); } +/** + * Runs the mocha executable in watch mode calls `change` and returns the + * raw result. + * + * The function starts mocha with the given arguments and `--watch` and + * waits until the first test run has completed. Then it calls `change` + * and waits until the second test run has been completed. Mocha is + * killed and the result is returned. + * + * On Windows, this will call `child_process.fork()` instead of `cross-spawn.spawn()`. + * + * **Exit code will always be 0** + * @param {string[]} args - Array of argument strings + * @param {object|string} opts - If a `string`, then `cwd`, otherwise options for `cross-spawn.spawn` or `child_process.fork` + * @param {Function} change - A potentially `Promise`-returning callback to execute which will change a watched file + * @returns {Promise} + */ +async function runMochaWatchAsync(args, opts, change) { + if (typeof opts === 'string') { + opts = {cwd: opts}; + } + opts = {sleepMs: 2000, ...opts, fork: process.platform === 'win32'}; + opts.stdio = ['pipe', 'pipe', 'inherit']; + const [mochaProcess, resultPromise] = invokeMochaAsync( + [...args, '--watch'], + opts + ); + await sleep(opts.sleepMs); + await change(mochaProcess); + await sleep(opts.sleepMs); + + if ( + !(mochaProcess.connected + ? mochaProcess.send('SIGINT') + : mochaProcess.kill('SIGINT')) + ) { + throw new Error('failed to send signal to subprocess'); + } + + const res = await resultPromise; + + // we kill the process with `SIGINT`, so it will always appear as "failed" to our + // custom assertions (a non-zero exit code 130). just change it to 0. + res.code = 0; + return res; +} + +/** + * Runs the mocha executable in watch mode calls `change` and returns the + * JSON result. + * + * The function starts mocha with the given arguments and `--watch` and + * waits until the first test run has completed. Then it calls `change` + * and waits until the second test run has been completed. Mocha is + * killed and the result is returned. + * + * On Windows, this will call `child_process.fork()` instead of `cross-spawn.spawn()`. + * + * **Exit code will always be 0** + * @param {string[]} args - Array of argument strings + * @param {object|string} opts - If a `string`, then `cwd`, otherwise options for `cross-spawn.spawn` or `child_process.fork` + * @param {Function} change - A potentially `Promise`-returning callback to execute which will change a watched file + * @returns {Promise} + */ +async function runMochaWatchJSONAsync(args, opts, change) { + const res = await runMochaWatchAsync( + [...args, '--reporter', 'json'], + opts, + change + ); + return ( + res.output + // eslint-disable-next-line no-control-regex + .replace(/\u001b\[\?25./g, '') + .split('\u001b[2K') + .map(x => JSON.parse(x)) + ); +} + +/** + * Synchronously touch a file. Creates + * the file and all its parent directories if necessary. + * + * @param {string} filepath - Path to file + */ +function touchFile(filepath) { + fs.ensureDirSync(path.dirname(filepath)); + touch.sync(filepath); +} + +/** + * Synchronously replace all substrings matched by `pattern` with + * `replacement` in the contents of file at `filepath` + * + * @param {string} filepath - Path to file + * @param {RegExp|string} pattern - Search pattern + * @param {string} replacement - Replacement + */ +function replaceFileContents(filepath, pattern, replacement) { + const contents = fs.readFileSync(filepath, 'utf-8'); + const newContents = contents.replace(pattern, replacement); + fs.writeFileSync(filepath, newContents, 'utf-8'); +} + +/** + * Synchronously copy a fixture to the given destination file path. + * Creates parent directories of the destination path if necessary. + * + * @param {string} fixtureName - Relative path from __dirname to fixture, or absolute path + * @param {*} dest - Destination directory + */ +function copyFixture(fixtureName, dest) { + const fixtureSource = resolveFixturePath(fixtureName); + fs.ensureDirSync(path.dirname(dest)); + fs.copySync(fixtureSource, dest); +} + +/** + * Creates a temporary directory + * @returns {Promise} Temp dir path and cleanup function + */ +const createTempDir = async () => { + const dirpath = await fs.mkdtemp(path.join(os.tmpdir(), 'mocha-')); + return { + dirpath, + removeTempDir: () => { + rimraf(dirpath); + } + }; +}; + +/** + * Waits for `time` ms. + * @param {number} time - Time in ms + * @returns {Promise} + */ +function sleep(time) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +} + +module.exports = { + DEFAULT_FIXTURE, + SPLIT_DOT_REPORTER_REGEXP, + + createTempDir, + invokeMocha, + invokeMochaAsync, + invokeNode, + getSummary, + resolveFixturePath, + toJSONResult, + escapeRegExp, + runMocha, + runMochaJSON, + runMochaAsync, + runMochaJSONAsync, + runMochaWatchAsync, + runMochaWatchJSONAsync, + copyFixture, + touchFile, + replaceFileContents +}; + /** * A summary of a `mocha` run * @typedef {Object} Summary @@ -421,3 +567,66 @@ function getSummary(res) { * @property {number} pending - Number of pending tests * @property {number} failing - Number of failing tests */ + +/** + * An unprocessed result from a `mocha` run + * @typedef {Object} RawResult + * @property {string} output - Process output; _usually_ just stdout + * @property {number?} code - Exit code or `null` in some circumstances + * @property {string[]} args - Array of program arguments + * @property {string} command - Complete command executed + */ + +/** + * The result of a `mocha` run using `json` reporter + * @typedef {Object} JSONResult + * @property {Object} stats - Statistics + * @property {Object[]} failures - Failure information + * @property {number?} code - Exit code or `null` in some circumstances + * @property {string} command - Complete command executed + */ + +/** + * The result of a `mocha` run using `spec` reporter (parsed) + * @typedef {Summary} SummarizedResult + * @property {string} output - Process output; _usually_ just stdout + * @property {number?} code - Exit code or `null` in some circumstances + */ + +/** + * Callback function run when `mocha` process execution complete + * @callback RawResultCallback + * @param {Error?} err - Error, if any + * @param {RawResult} result - Result of `mocha` run + * @returns {void} + */ + +/** + * Callback function run when `mocha` process execution complete + * @callback JSONResultCallback + * @param {Error?} err - Error, if any + * @param {JSONResult} result - Result of `mocha` run + * @returns {void} + */ + +/** + * Callback function run when `mocha` process execution complete + * @callback SummarizedResultCallback + * @param {Error?} err - Error, if any + * @param {SummarizedResult} result - Result of `mocha` run + * @returns {void} + */ + +/** + * Return value when calling {@link createTempDir} + * + * @typedef {Object} CreateTempDirResult + * @property {string} dirname - Path of new temp dir + * @property {RemoveTempDirCallback} removeTempDir - "Cleanup" function to remove temp dir + */ + +/** + * Cleanup function to remove temp dir + * @callback RemoveTempDirCallback + * @returns {void} + */ diff --git a/test/integration/hook-err.spec.js b/test/integration/hook-err.spec.js index d5fe6e858d..ad327dfc05 100644 --- a/test/integration/hook-err.spec.js +++ b/test/integration/hook-err.spec.js @@ -3,7 +3,7 @@ var helpers = require('./helpers'); var runMocha = helpers.runMocha; var runMochaJSON = require('./helpers').runMochaJSON; -var splitRegExp = helpers.splitRegExp; +var SPLIT_DOT_REPORTER_REGEXP = helpers.SPLIT_DOT_REPORTER_REGEXP; var bang = require('../../lib/reporters/base').symbols.bang; describe('hook error handling', function() { @@ -263,7 +263,7 @@ describe('hook error handling', function() { expect(err, 'to be falsy'); lines = res.output - .split(splitRegExp) + .split(SPLIT_DOT_REPORTER_REGEXP) .map(function(line) { return line.trim(); }) diff --git a/test/integration/hooks.spec.js b/test/integration/hooks.spec.js index 7c3e8bf2b1..727ce1156d 100644 --- a/test/integration/hooks.spec.js +++ b/test/integration/hooks.spec.js @@ -3,7 +3,7 @@ var assert = require('assert'); var runMocha = require('./helpers').runMocha; var runMochaJSON = require('./helpers').runMochaJSON; -var splitRegExp = require('./helpers').splitRegExp; +var SPLIT_DOT_REPORTER_REGEXP = require('./helpers').SPLIT_DOT_REPORTER_REGEXP; var args = ['--reporter', 'dot']; describe('hooks', function() { @@ -17,7 +17,7 @@ describe('hooks', function() { } lines = res.output - .split(splitRegExp) + .split(SPLIT_DOT_REPORTER_REGEXP) .map(function(line) { return line.trim(); }) diff --git a/test/integration/multiple-runs.spec.js b/test/integration/multiple-runs.spec.js index 61d672d4b2..255281430e 100644 --- a/test/integration/multiple-runs.spec.js +++ b/test/integration/multiple-runs.spec.js @@ -1,14 +1,17 @@ 'use strict'; -var invokeNode = require('./helpers').invokeNode; +const {invokeNode} = require('./helpers'); -describe('multiple runs', function(done) { +describe('multiple runs', function() { it('should be allowed to run multiple times if cleanReferences is turned off', function(done) { var path = require.resolve( './fixtures/multiple-runs/run-thrice.fixture.js' ); invokeNode([path], function(err, res) { - expect(err, 'to be null'); + if (err) { + done(err); + return; + } expect(res.code, 'to be', 0); var results = JSON.parse(res.output); expect(results, 'to have length', 3); @@ -32,9 +35,15 @@ describe('multiple runs', function(done) { invokeNode( [path], function(err, res) { - expect(err, 'to be null'); - expect(res.code, 'not to be', 0); - expect(res.output, 'to contain', 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED'); + if (err) { + done(err); + return; + } + expect(res, 'to have failed').and( + 'to contain output', + /ERR_MOCHA_INSTANCE_ALREADY_DISPOSED/ + ); + done(); }, {stdio: ['ignore', 'pipe', 'pipe']} @@ -46,7 +55,10 @@ describe('multiple runs', function(done) { invokeNode( [path, '--directly-dispose'], function(err, res) { - expect(err, 'to be null'); + if (err) { + done(err); + return; + } expect(res.code, 'not to be', 0); expect(res.output, 'to contain', 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED'); done(); @@ -62,7 +74,10 @@ describe('multiple runs', function(done) { invokeNode( [path], function(err, res) { - expect(err, 'to be null'); + if (err) { + done(err); + return; + } expect(res.output, 'to contain', 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING'); done(); }, diff --git a/test/integration/options/extension.spec.js b/test/integration/options/extension.spec.js index 760e3bcd88..56e40f6bad 100644 --- a/test/integration/options/extension.spec.js +++ b/test/integration/options/extension.spec.js @@ -2,7 +2,7 @@ var helpers = require('../helpers'); var invokeMocha = helpers.invokeMocha; -var toJSONRunResult = helpers.toJSONRunResult; +var toJSONResult = helpers.toJSONResult; describe('--extension', function() { it('should allow comma-separated variables', function(done) { @@ -21,7 +21,7 @@ describe('--extension', function() { if (err) { return done(err); } - expect(toJSONRunResult(res), 'to have passed').and( + expect(toJSONResult(res), 'to have passed').and( 'to have passed test count', 2 ); diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js index 822b28b6da..763407eef2 100644 --- a/test/integration/options/watch.spec.js +++ b/test/integration/options/watch.spec.js @@ -1,30 +1,44 @@ 'use strict'; const fs = require('fs-extra'); -const os = require('os'); const path = require('path'); -const helpers = require('../helpers'); +const { + copyFixture, + runMochaWatchJSONAsync, + touchFile, + replaceFileContents, + createTempDir, + DEFAULT_FIXTURE +} = require('../helpers'); describe('--watch', function() { describe('when enabled', function() { + /** + * @type {string} + */ let tempDir; + /** + * @type {import('../helpers').RemoveTempDirCallback} + */ + let cleanup; + this.slow(5000); - beforeEach(function() { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-')); + beforeEach(async function() { + const {dirpath, removeTempDir} = await createTempDir(); + tempDir = dirpath; + cleanup = removeTempDir; }); afterEach(function() { - if (tempDir) { - return fs.remove(tempDir); - } + cleanup(); }); it('reruns test when watched test file is touched', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); - return runMochaWatch([testFile], tempDir, () => { + return runMochaWatchJSONAsync([testFile], tempDir, () => { touchFile(testFile); }).then(results => { expect(results, 'to have length', 2); @@ -34,9 +48,9 @@ describe('--watch', function() { describe('when in parallel mode', function() { it('reruns test when watched test file is touched', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); - return runMochaWatch(['--parallel', testFile], tempDir, () => { + return runMochaWatchJSONAsync(['--parallel', testFile], tempDir, () => { touchFile(testFile); }).then(results => { expect(results, 'to have length', 2); @@ -46,12 +60,12 @@ describe('--watch', function() { it('reruns test when file matching --watch-files changes', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'dir/file.xyz'); touchFile(watchedFile); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', 'dir/*.xyz'], tempDir, () => { @@ -64,10 +78,10 @@ describe('--watch', function() { it('reruns test when file matching --watch-files is added', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'lib/file.xyz'); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', '**/*.xyz'], tempDir, () => { @@ -80,12 +94,12 @@ describe('--watch', function() { it('reruns test when file matching --watch-files is removed', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'lib/file.xyz'); touchFile(watchedFile); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', 'lib/**/*.xyz'], tempDir, () => { @@ -98,12 +112,12 @@ describe('--watch', function() { it('does not rerun test when file not matching --watch-files is changed', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'dir/file.js'); touchFile(watchedFile); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', 'dir/*.xyz'], tempDir, () => { @@ -116,9 +130,9 @@ describe('--watch', function() { it('picks up new test files when they are added', function() { const testFile = path.join(tempDir, 'test/a.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); - return runMochaWatch( + return runMochaWatchJSONAsync( ['test/**/*.js', '--watch-files', 'test/**/*.js'], tempDir, () => { @@ -134,23 +148,27 @@ describe('--watch', function() { it('reruns test when file matching --extension is changed', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'file.xyz'); touchFile(watchedFile); - return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { - touchFile(watchedFile); - }).then(results => { + return runMochaWatchJSONAsync( + [testFile, '--extension', 'xyz,js'], + tempDir, + () => { + touchFile(watchedFile); + } + ).then(results => { expect(results, 'to have length', 2); }); }); it('reruns when "rs\\n" typed', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); - return runMochaWatch([testFile], tempDir, mochaProcess => { + return runMochaWatchJSONAsync([testFile], tempDir, mochaProcess => { mochaProcess.stdin.write('rs\n'); }).then(results => { expect(results, 'to have length', 2); @@ -159,21 +177,25 @@ describe('--watch', function() { it('reruns test when file starting with . and matching --extension is changed', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, '.file.xyz'); touchFile(watchedFile); - return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { - touchFile(watchedFile); - }).then(results => { + return runMochaWatchJSONAsync( + [testFile, '--extension', 'xyz,js'], + tempDir, + () => { + touchFile(watchedFile); + } + ).then(results => { expect(results, 'to have length', 2); }); }); it('ignores files in "node_modules" and ".git" by default', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const nodeModulesFile = path.join(tempDir, 'node_modules', 'file.xyz'); const gitFile = path.join(tempDir, '.git', 'file.xyz'); @@ -181,22 +203,26 @@ describe('--watch', function() { touchFile(gitFile); touchFile(nodeModulesFile); - return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { - touchFile(gitFile); - touchFile(nodeModulesFile); - }).then(results => { + return runMochaWatchJSONAsync( + [testFile, '--extension', 'xyz,js'], + tempDir, + () => { + touchFile(gitFile); + touchFile(nodeModulesFile); + } + ).then(results => { expect(results, 'to have length', 1); }); }); it('ignores files matching --watch-ignore', function() { const testFile = path.join(tempDir, 'test.js'); - copyFixture('__default__', testFile); + copyFixture(DEFAULT_FIXTURE, testFile); const watchedFile = path.join(tempDir, 'dir/file-to-ignore.xyz'); touchFile(watchedFile); - return runMochaWatch( + return runMochaWatchJSONAsync( [ testFile, '--watch-files', @@ -217,7 +243,7 @@ describe('--watch', function() { const testFile = path.join(tempDir, 'test.js'); copyFixture('options/watch/test-file-change', testFile); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', '**/*.js'], tempDir, () => { @@ -243,7 +269,7 @@ describe('--watch', function() { const dependency = path.join(tempDir, 'lib', 'dependency.js'); copyFixture('options/watch/dependency', dependency); - return runMochaWatch( + return runMochaWatchJSONAsync( [testFile, '--watch-files', 'lib/**/*.js'], tempDir, () => { @@ -263,17 +289,22 @@ describe('--watch', function() { }); // Regression test for https://github.com/mochajs/mocha/issues/2027 - it('respects --fgrep on re-runs', function() { + it('respects --fgrep on re-runs', async function() { const testFile = path.join(tempDir, 'test.js'); copyFixture('options/grep', testFile); - return runMochaWatch([testFile, '--fgrep', 'match'], tempDir, () => { - touchFile(testFile); - }).then(results => { - expect(results, 'to have length', 2); - expect(results[0].tests, 'to have length', 2); - expect(results[1].tests, 'to have length', 2); - }); + return expect( + runMochaWatchJSONAsync([testFile, '--fgrep', 'match'], tempDir, () => { + touchFile(testFile); + }), + 'when fulfilled', + 'to satisfy', + { + length: 2, + 0: {tests: expect.it('to have length', 2)}, + 1: {tests: expect.it('to have length', 2)} + } + ); }); describe('with required hooks', function() { @@ -314,70 +345,3 @@ describe('--watch', function() { }); }); }); - -/** - * Runs the mocha binary in watch mode calls `change` and returns the - * JSON reporter output. - * - * The function starts mocha with the given arguments and `--watch` and - * waits until the first test run has completed. Then it calls `change` - * and waits until the second test run has been completed. Mocha is - * killed and the list of JSON outputs is returned. - */ -function runMochaWatch(args, cwd, change) { - const [mochaProcess, resultPromise] = helpers.invokeMochaAsync( - [...args, '--watch', '--reporter', 'json'], - {cwd, stdio: ['pipe', 'pipe', 'inherit']} - ); - - return sleep(2000) - .then(() => change(mochaProcess)) - .then(() => sleep(2000)) - .then(() => { - mochaProcess.kill('SIGINT'); - return resultPromise; - }) - .then(data => { - const testResults = data.output - // eslint-disable-next-line no-control-regex - .replace(/\u001b\[\?25./g, '') - .split('\u001b[2K') - .map(x => JSON.parse(x)); - return testResults; - }); -} - -/** - * Synchronously touch a file by appending a space to the end. Creates - * the file and all its parent directories if necessary. - */ -function touchFile(file) { - fs.ensureDirSync(path.dirname(file)); - fs.appendFileSync(file, ' '); -} - -/** - * Synchronously replace all substrings matched by `pattern` with - * `replacement` in the file’s content. - */ -function replaceFileContents(file, pattern, replacement) { - const contents = fs.readFileSync(file, 'utf-8'); - const newContents = contents.replace(pattern, replacement); - fs.writeFileSync(file, newContents, 'utf-8'); -} - -/** - * Synchronously copy a fixture to the given destination file path. - * Creates parent directories of the destination path if necessary. - */ -function copyFixture(fixtureName, dest) { - const fixtureSource = helpers.resolveFixturePath(fixtureName); - fs.ensureDirSync(path.dirname(dest)); - fs.copySync(fixtureSource, dest); -} - -function sleep(time) { - return new Promise(resolve => { - setTimeout(resolve, time); - }); -} diff --git a/test/integration/pending.spec.js b/test/integration/pending.spec.js index 51a7bc9b16..5fe75f7c2d 100644 --- a/test/integration/pending.spec.js +++ b/test/integration/pending.spec.js @@ -4,7 +4,7 @@ var assert = require('assert'); var helpers = require('./helpers'); var run = helpers.runMochaJSON; var invokeNode = helpers.invokeNode; -var toJSONRunResult = helpers.toJSONRunResult; +var toJSONResult = helpers.toJSONResult; var args = []; describe('pending', function() { @@ -305,7 +305,7 @@ describe('pending', function() { if (err) { return done(err); } - var result = toJSONRunResult(res); + var result = toJSONResult(res); expect(result, 'to have passed') .and('to have passed test count', 0) .and('to have pending test count', 1) diff --git a/test/integration/plugins/global-setup-teardown.spec.js b/test/integration/plugins/global-setup-teardown.spec.js new file mode 100644 index 0000000000..a5433014aa --- /dev/null +++ b/test/integration/plugins/global-setup-teardown.spec.js @@ -0,0 +1,226 @@ +'use strict'; + +const os = require('os'); +const fs = require('fs-extra'); +const path = require('path'); +const { + touchFile, + runMochaAsync, + runMochaWatchAsync, + copyFixture, + DEFAULT_FIXTURE, + resolveFixturePath +} = require('../helpers'); + +describe('global setup/teardown', function() { + describe('when mocha run in serial mode', function() { + it('should execute global setup and teardown', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--require', + resolveFixturePath( + 'plugins/global-setup-teardown/global-setup-teardown' + ) + ]), + 'when fulfilled', + 'to have passed' + ); + }); + + it('should share context', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--require', + resolveFixturePath( + 'plugins/global-setup-teardown/global-setup-teardown' + ) + ]), + 'when fulfilled', + 'to contain', + /setup: this\.foo = bar[\s\S]+teardown: this\.foo = bar/ + ); + }); + + describe('when supplied multiple functions', function() { + it('should execute them sequentially', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--require', + resolveFixturePath( + 'plugins/global-setup-teardown/global-setup-teardown-multiple' + ) + ]), + 'when fulfilled', + 'to contain', + /teardown: this.foo = 3/ + ); + }); + }); + + describe('when run in watch mode', function() { + let tempDir; + let testFile; + + beforeEach(async function() { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mocha-')); + testFile = path.join(tempDir, 'test.js'); + copyFixture(DEFAULT_FIXTURE, testFile); + }); + + afterEach(async function() { + if (tempDir) { + return fs.remove(tempDir); + } + }); + + it('should execute global setup and teardown', async function() { + return expect( + runMochaWatchAsync( + [ + '--require', + resolveFixturePath( + 'plugins/global-setup-teardown/global-setup-teardown' + ), + testFile + ], + tempDir, + () => { + touchFile(testFile); + } + ), + 'when fulfilled', + 'to have passed' + ); + }); + + it('should not re-execute the global fixtures', async function() { + return expect( + runMochaWatchAsync( + [ + '--require', + resolveFixturePath( + 'plugins/global-setup-teardown/global-setup-teardown-multiple' + ), + testFile + ], + tempDir, + () => { + touchFile(testFile); + } + ), + 'when fulfilled', + 'to contain once', + /teardown: this.foo = 3/ + ); + }); + }); + }); + + describe('when mocha run in parallel mode', function() { + it('should execute global setup and teardown', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--parallel', + '--require', + require.resolve( + '../fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js' + ) + ]), + 'when fulfilled', + 'to have passed' + ); + }); + + it('should share context', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--parallel', + '--require', + require.resolve( + '../fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js' + ) + ]), + 'when fulfilled', + 'to contain', + /setup: this.foo = bar/ + ).and('when fulfilled', 'to contain', /teardown: this.foo = bar/); + }); + + describe('when supplied multiple functions', function() { + it('should execute them sequentially', async function() { + return expect( + runMochaAsync(DEFAULT_FIXTURE, [ + '--parallel', + '--require', + require.resolve( + '../fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js' + ) + ]), + 'when fulfilled', + 'to contain', + /teardown: this.foo = 3/ + ); + }); + }); + + describe('when run in watch mode', function() { + let tempDir; + let testFile; + + beforeEach(async function() { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mocha-')); + testFile = path.join(tempDir, 'test.js'); + copyFixture(DEFAULT_FIXTURE, testFile); + }); + + afterEach(async function() { + if (tempDir) { + return fs.remove(tempDir); + } + }); + + it('should execute global setup and teardown', async function() { + return expect( + runMochaWatchAsync( + [ + '--parallel', + '--require', + require.resolve( + '../fixtures/plugins/global-setup-teardown/global-setup-teardown.fixture.js' + ), + testFile + ], + tempDir, + () => { + touchFile(testFile); + } + ), + 'when fulfilled', + 'to have passed' + ); + }); + + it('should not re-execute the global fixtures', async function() { + return expect( + runMochaWatchAsync( + [ + '--parallel', + '--require', + require.resolve( + '../fixtures/plugins/global-setup-teardown/global-setup-teardown-multiple.fixture.js' + ), + testFile + ], + tempDir, + () => { + touchFile(testFile); + } + ), + 'when fulfilled', + 'to contain once', + /teardown: this.foo = 3/ + ); + }); + }); + }); +}); diff --git a/test/integration/options/require.spec.js b/test/integration/plugins/root-hooks.spec.js similarity index 81% rename from test/integration/options/require.spec.js rename to test/integration/plugins/root-hooks.spec.js index 21da366b2e..53d18d58d0 100644 --- a/test/integration/options/require.spec.js +++ b/test/integration/plugins/root-hooks.spec.js @@ -30,21 +30,21 @@ function runMochaForHookOutput(args, opts) { return invokeMochaAsync(args, opts)[1].then(extractHookOutputFromResult); } -describe('--require', function() { +describe('root hooks', function() { describe('when mocha run in serial mode', function() { it('should run root hooks when provided via mochaHooks object export', function() { return expect( runMochaForHookOutput([ '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-a.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js' ), '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-b.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-b.fixture.js' ), require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ) ]), 'to be fulfilled with', @@ -70,14 +70,14 @@ describe('--require', function() { runMochaForHookOutput([ '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-c.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-c.fixture.js' ), '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-d.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-d.fixture.js' ), require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ) ]), 'to be fulfilled with', @@ -110,20 +110,20 @@ describe('--require', function() { '--require=' + require.resolve( // as object - '../fixtures/options/require/root-hook-defs-esm.fixture.mjs' + '../fixtures/plugins/root-hooks/root-hook-defs-esm.fixture.mjs' ), '--require=' + require.resolve( // as function - '../fixtures/options/require/esm/root-hook-defs-esm.fixture.js' + '../fixtures/plugins/root-hooks/esm/root-hook-defs-esm.fixture.js' ), '--require=' + require.resolve( // mixed with commonjs - '../fixtures/options/require/root-hook-defs-a.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js' ), require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ) ].concat( +process.versions.node.split('.')[0] >= 13 @@ -158,7 +158,7 @@ describe('--require', function() { '--require=' + require.resolve( // as object - '../fixtures/options/require/root-hook-defs-esm-broken.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-esm-broken.fixture.js' ) ].concat( +process.versions.node.split('.')[0] >= 13 @@ -181,15 +181,15 @@ describe('--require', function() { runMochaForHookOutput([ '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-a.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js' ), '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-b.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-b.fixture.js' ), '--parallel', require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ) ]), 'to be fulfilled with', @@ -215,15 +215,15 @@ describe('--require', function() { runMochaForHookOutput([ '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-c.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-c.fixture.js' ), '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-d.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-d.fixture.js' ), '--parallel', require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ) ]), 'to be fulfilled with', @@ -250,18 +250,18 @@ describe('--require', function() { runMochaForHookOutput([ '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-a.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-a.fixture.js' ), '--require=' + require.resolve( - '../fixtures/options/require/root-hook-defs-b.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-defs-b.fixture.js' ), '--parallel', require.resolve( - '../fixtures/options/require/root-hook-test.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test.fixture.js' ), require.resolve( - '../fixtures/options/require/root-hook-test-2.fixture.js' + '../fixtures/plugins/root-hooks/root-hook-test-2.fixture.js' ) ]), 'to be fulfilled with', diff --git a/test/integration/retries.spec.js b/test/integration/retries.spec.js index e076595d7d..bb5ae51898 100644 --- a/test/integration/retries.spec.js +++ b/test/integration/retries.spec.js @@ -20,7 +20,7 @@ describe('retries', function() { } lines = res.output - .split(helpers.splitRegExp) + .split(helpers.SPLIT_DOT_REPORTER_REGEXP) .map(function(line) { return line.trim(); }) @@ -102,7 +102,7 @@ describe('retries', function() { } lines = res.output - .split(helpers.splitRegExp) + .split(helpers.SPLIT_DOT_REPORTER_REGEXP) .map(function(line) { return line.trim(); }) diff --git a/test/node-unit/cli/run-helpers.spec.js b/test/node-unit/cli/run-helpers.spec.js index 3169bbd0bb..e12b97b875 100644 --- a/test/node-unit/cli/run-helpers.spec.js +++ b/test/node-unit/cli/run-helpers.spec.js @@ -1,76 +1,13 @@ 'use strict'; -const { - validatePlugin, - list, - loadRootHooks -} = require('../../../lib/cli/run-helpers'); +const {validateLegacyPlugin, list} = require('../../../lib/cli/run-helpers'); describe('helpers', function() { - describe('loadRootHooks()', function() { - describe('when passed nothing', function() { - it('should reject', async function() { - return expect(loadRootHooks(), 'to be rejected'); - }); - }); - - describe('when passed empty array of hooks', function() { - it('should return an empty MochaRootHooks object', async function() { - return expect(loadRootHooks([]), 'to be fulfilled with', { - beforeAll: [], - beforeEach: [], - afterAll: [], - afterEach: [] - }); - }); - }); - - describe('when passed an array containing hook objects and sync functions and async functions', function() { - it('should flatten them into a single object', async function() { - function a() {} - function b() {} - function d() {} - function g() {} - async function f() {} - function c() { - return { - beforeAll: d, - beforeEach: g - }; - } - async function e() { - return { - afterEach: f - }; - } - return expect( - loadRootHooks([ - { - beforeEach: a - }, - { - afterAll: b - }, - c, - e - ]), - 'to be fulfilled with', - { - beforeAll: [d], - beforeEach: [a, g], - afterAll: [b], - afterEach: [f] - } - ); - }); - }); - }); - - describe('validatePlugin()', function() { + describe('validateLegacyPlugin()', function() { describe('when used with "reporter" key', function() { it('should disallow an array of names', function() { expect( - () => validatePlugin({reporter: ['bar']}, 'reporter'), + () => validateLegacyPlugin({reporter: ['bar']}, 'reporter'), 'to throw', { code: 'ERR_MOCHA_INVALID_REPORTER', @@ -81,7 +18,7 @@ describe('helpers', function() { it('should fail to recognize an unknown reporter', function() { expect( - () => validatePlugin({reporter: 'bar'}, 'reporter'), + () => validateLegacyPlugin({reporter: 'bar'}, 'reporter'), 'to throw', {code: 'ERR_MOCHA_INVALID_REPORTER', message: /cannot find module/i} ); @@ -91,7 +28,7 @@ describe('helpers', function() { describe('when used with an "interfaces" key', function() { it('should disallow an array of names', function() { expect( - () => validatePlugin({interface: ['bar']}, 'interface'), + () => validateLegacyPlugin({interface: ['bar']}, 'interface'), 'to throw', { code: 'ERR_MOCHA_INVALID_INTERFACE', @@ -102,7 +39,7 @@ describe('helpers', function() { it('should fail to recognize an unknown interface', function() { expect( - () => validatePlugin({interface: 'bar'}, 'interface'), + () => validateLegacyPlugin({interface: 'bar'}, 'interface'), 'to throw', {code: 'ERR_MOCHA_INVALID_INTERFACE', message: /cannot find module/i} ); @@ -112,7 +49,7 @@ describe('helpers', function() { describe('when used with an unknown plugin type', function() { it('should fail', function() { expect( - () => validatePlugin({frog: 'bar'}, 'frog'), + () => validateLegacyPlugin({frog: 'bar'}, 'frog'), 'to throw', /unknown plugin/i ); @@ -123,7 +60,7 @@ describe('helpers', function() { it('should fail and report the original error', function() { expect( () => - validatePlugin( + validateLegacyPlugin( { reporter: require.resolve('./fixtures/bad-module.fixture.js') }, diff --git a/test/node-unit/worker.spec.js b/test/node-unit/worker.spec.js index 8d46ef4973..4081708905 100644 --- a/test/node-unit/worker.spec.js +++ b/test/node-unit/worker.spec.js @@ -54,16 +54,20 @@ describe('worker', function() { }; stubs.runHelpers = { - handleRequires: sinon.stub(), - validatePlugin: sinon.stub(), - loadRootHooks: sinon.stub().resolves() + handleRequires: sinon.stub().resolves({}), + validateLegacyPlugin: sinon.stub() + }; + + stubs.plugin = { + aggregateRootHooks: sinon.stub().resolves() }; worker = rewiremock.proxy(WORKER_PATH, { workerpool: stubs.workerpool, '../../lib/mocha': stubs.Mocha, '../../lib/nodejs/serializer': stubs.serializer, - '../../lib/cli/run-helpers': stubs.runHelpers + '../../lib/cli/run-helpers': stubs.runHelpers, + '../../lib/plugin-loader': stubs.plugin }); }); @@ -155,7 +159,7 @@ describe('worker', function() { await worker.run('some-file.js', serializeJavascript(argv)); expect( - stubs.runHelpers.validatePlugin, + stubs.runHelpers.validateLegacyPlugin, 'to have a call satisfying', [argv, 'ui', stubs.Mocha.interfaces] ).and('was called once'); @@ -204,7 +208,7 @@ describe('worker', function() { expect(stubs.runHelpers, 'to satisfy', { handleRequires: expect.it('was called once'), - validatePlugin: expect.it('was called once') + validateLegacyPlugin: expect.it('was called once') }); }); }); diff --git a/test/reporters/xunit.spec.js b/test/reporters/xunit.spec.js index f3dfe21e55..1d3bad9cb5 100644 --- a/test/reporters/xunit.spec.js +++ b/test/reporters/xunit.spec.js @@ -2,15 +2,15 @@ var EventEmitter = require('events').EventEmitter; var fs = require('fs'); -var os = require('os'); var path = require('path'); -var rimraf = require('rimraf'); var sinon = require('sinon'); var createStatsCollector = require('../../lib/stats-collector'); var events = require('../../').Runner.constants; var reporters = require('../../').reporters; var states = require('../../').Runnable.constants; +const {createTempDir, touchFile} = require('../integration/helpers'); + var Base = reporters.Base; var XUnit = reporters.XUnit; @@ -81,15 +81,21 @@ describe('XUnit reporter', function() { describe('when fileStream cannot be created', function() { describe('when given an invalid pathname', function() { - var tmpdir; + /** + * @type {string} + */ + let tmpdir; + + /** + * @type {import('../integration/helpers').RemoveTempDirCallback} + */ + let cleanup; var invalidPath; - beforeEach(function createInvalidPath() { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-test-')); - - function touch(filename) { - fs.closeSync(fs.openSync(filename, 'w')); - } + beforeEach(async function() { + const {dirpath, removeTempDir} = await createTempDir(); + tmpdir = dirpath; + cleanup = removeTempDir; // Create path where file 'some-file' used as directory invalidPath = path.join( @@ -97,7 +103,7 @@ describe('XUnit reporter', function() { 'some-file', path.basename(expectedOutput) ); - touch(path.dirname(invalidPath)); + touchFile(path.dirname(invalidPath)); }); it('should throw system error', function() { @@ -119,7 +125,7 @@ describe('XUnit reporter', function() { }); afterEach(function() { - rimraf.sync(tmpdir); + cleanup(); }); }); diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index 75be0341c9..ad7c4bc0fc 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -722,7 +722,8 @@ describe('Mocha', function() { mocha.run(); } catch (ignored) { } finally { - expect(runner.run, 'was called once'); + // it'll be 0 or 1, depending on timing. + expect(runner.run.callCount, 'to be less than', 2); } }); }); @@ -844,5 +845,86 @@ describe('Mocha', function() { }); }); }); + + describe('_runGlobalFixtures()', function() { + it('should execute multiple fixtures in order', async function() { + const fixtures = [ + sinon.stub().resolves('foo'), + sinon.stub().returns('bar') + ]; + const context = await mocha._runGlobalFixtures(fixtures); + + return expect(fixtures, 'to satisfy', [ + expect.it('to have a call satisfying', { + thisValue: context, + returnValue: expect.it('to be fulfilled with', 'foo') + }), + expect.it('to have a call satisfying', { + thisValue: context, + returnValue: 'bar' + }) + ]); + }); + }); + + describe('runGlobalSetup()', function() { + let context; + + beforeEach(function() { + sinon.stub(mocha, '_runGlobalFixtures').resolvesArg(1); + context = {}; + }); + + describe('when a fixture is present', function() { + beforeEach(function() { + mocha.options.globalSetup = [sinon.spy()]; + }); + + it('should call _runGlobalFixtures()', async function() { + await mocha.runGlobalSetup(context); + expect(mocha._runGlobalFixtures, 'to have a call satisfying', [ + mocha.options.globalSetup, + context + ]); + }); + }); + + describe('when a fixture is not present', function() { + it('should not call _runGlobalFixtures()', async function() { + await mocha.runGlobalSetup(); + expect(mocha._runGlobalFixtures, 'was not called'); + }); + }); + }); + + describe('runGlobalTeardown()', function() { + let context; + + beforeEach(function() { + sinon.stub(mocha, '_runGlobalFixtures').resolvesArg(1); + context = {}; + }); + + describe('when a fixture is present', function() { + beforeEach(function() { + mocha.options.globalTeardown = [sinon.spy()]; + }); + + it('should call _runGlobalFixtures()', async function() { + await mocha.runGlobalTeardown(); + expect(mocha._runGlobalFixtures, 'to have a call satisfying', [ + mocha.options.globalTeardown, + context + ]); + }); + }); + + describe('when a fixture is not present', function() { + it('should not call _runGlobalFixtures()', async function() { + await mocha.runGlobalTeardown(); + expect(mocha._runGlobalFixtures, 'was not called'); + }); + }); + }); }); }); diff --git a/test/unit/plugin-loader.spec.js b/test/unit/plugin-loader.spec.js new file mode 100644 index 0000000000..b72bee617a --- /dev/null +++ b/test/unit/plugin-loader.spec.js @@ -0,0 +1,465 @@ +'use strict'; + +const PluginLoader = require('../../lib/plugin-loader'); +const sinon = require('sinon'); +const { + INVALID_PLUGIN_DEFINITION, + INVALID_PLUGIN_IMPLEMENTATION +} = require('../../lib/errors').constants; + +describe('plugin module', function() { + describe('class PluginLoader', function() { + describe('constructor', function() { + describe('when passed no options', function() { + it('should populate a registry of built-in plugins', function() { + expect(new PluginLoader().registered.has('mochaHooks'), 'to be true'); + }); + }); + + describe('when passed custom plugins', function() { + it('should register the custom plugins', function() { + const plugin = {exportName: 'mochaBananaPhone'}; + expect( + new PluginLoader([plugin]).registered.get('mochaBananaPhone'), + 'to equal', + plugin + ); + }); + }); + }); + + describe('static method', function() { + describe('create()', function() { + it('should return a PluginLoader instance', function() { + expect(PluginLoader.create(), 'to be a', PluginLoader); + }); + }); + }); + + describe('instance method', function() { + let pluginLoader; + + beforeEach(function() { + pluginLoader = PluginLoader.create(); + }); + + describe('register', function() { + describe('when the plugin export name is not in use', function() { + it('should not throw', function() { + expect( + () => pluginLoader.register({exportName: 'butts'}), + 'not to throw' + ); + }); + }); + + describe('when the plugin export name is already in use', function() { + it('should throw', function() { + const pluginDef = {exportName: 'butts'}; + pluginLoader.register(pluginDef); + expect(() => pluginLoader.register(pluginDef), 'to throw', { + code: INVALID_PLUGIN_DEFINITION, + pluginDef + }); + }); + }); + + describe('when passed a falsy parameter', function() { + it('should throw', function() { + expect(() => pluginLoader.register(), 'to throw', { + code: INVALID_PLUGIN_DEFINITION + }); + }); + }); + + describe('when passed a non-object parameter', function() { + it('should throw', function() { + expect(() => pluginLoader.register(1), 'to throw', { + code: INVALID_PLUGIN_DEFINITION, + pluginDef: 1 + }); + }); + }); + + describe('when passed a definition w/o an exportName', function() { + it('should throw', function() { + const pluginDef = {foo: 'bar'}; + expect(() => pluginLoader.register(pluginDef), 'to throw', { + code: INVALID_PLUGIN_DEFINITION, + pluginDef + }); + }); + }); + }); + + describe('load()', function() { + let pluginLoader; + + beforeEach(function() { + pluginLoader = PluginLoader.create(); + }); + + describe('when called with a falsy value', function() { + it('should return false', function() { + expect(pluginLoader.load(), 'to be false'); + }); + }); + + describe('when called with an object containing no recognized plugin', function() { + it('should return false', function() { + // also it should not throw + expect( + pluginLoader.load({mochaBananaPhone: () => {}}), + 'to be false' + ); + }); + }); + + describe('when called with an object containing a recognized plugin', function() { + let plugin; + let pluginLoader; + + beforeEach(function() { + plugin = { + exportName: 'mochaBananaPhone', + validate: sinon.spy() + }; + pluginLoader = PluginLoader.create([plugin]); + }); + + it('should return true', function() { + const func = () => {}; + expect(pluginLoader.load({mochaBananaPhone: func}), 'to be true'); + }); + + it('should retain the value of any matching property in its mapping', function() { + const func = () => {}; + pluginLoader.load({mochaBananaPhone: func}); + expect(pluginLoader.loaded.get('mochaBananaPhone'), 'to equal', [ + func + ]); + }); + + it('should call the associated validator, if present', function() { + const func = () => {}; + pluginLoader.load({mochaBananaPhone: func}); + expect(plugin.validate, 'was called once'); + }); + }); + }); + + describe('load()', function() { + let pluginLoader; + let fooPlugin; + let barPlugin; + + beforeEach(function() { + fooPlugin = { + exportName: 'foo', + validate: sinon.stub() + }; + fooPlugin.validate.withArgs('ERROR').throws(); + barPlugin = { + exportName: 'bar', + validate: sinon.stub() + }; + pluginLoader = PluginLoader.create([fooPlugin, barPlugin]); + }); + + describe('when passed a falsy or non-object value', function() { + it('should return false', function() { + expect(pluginLoader.load(), 'to be false'); + }); + + it('should not call a validator', function() { + expect([fooPlugin, barPlugin], 'to have items satisfying', { + validate: expect.it('was not called') + }); + }); + }); + + describe('when passed an object value', function() { + describe('when no keys match any known named exports', function() { + let retval; + + beforeEach(function() { + retval = pluginLoader.load({butts: () => {}}); + }); + + it('should return false', function() { + expect(retval, 'to be false'); + }); + }); + + describe('when a key matches a known named export', function() { + let retval; + let impl; + + beforeEach(function() { + impl = sinon.stub(); + }); + + it('should call the associated validator', function() { + retval = pluginLoader.load({foo: impl}); + + expect(fooPlugin.validate, 'to have a call satisfying', [ + impl + ]).and('was called once'); + }); + + it('should not call validators whose keys were not found', function() { + retval = pluginLoader.load({foo: impl}); + expect(barPlugin.validate, 'was not called'); + }); + + describe('when the value passes the associated validator', function() { + beforeEach(function() { + retval = pluginLoader.load({foo: impl}); + }); + + it('should return true', function() { + expect(retval, 'to be true'); + }); + + it('should add the implementation to the internal mapping', function() { + expect(pluginLoader.loaded.get('foo'), 'to have length', 1); + }); + + it('should not add an implementation of plugins not present', function() { + expect(pluginLoader.loaded.get('bar'), 'to be empty'); + }); + }); + + describe('when the value does not pass the associated validator', function() { + it('should throw', function() { + expect(() => pluginLoader.load({foo: 'ERROR'}), 'to throw'); + }); + }); + }); + }); + }); + + describe('finalize()', function() { + let pluginLoader; + let fooPlugin; + let barPlugin; + let bazPlugin; + + beforeEach(function() { + fooPlugin = { + exportName: 'foo', + optionName: 'fooOption', + validate: sinon.stub(), + finalize: impls => impls.map(() => 'FOO') + }; + fooPlugin.validate.withArgs('ERROR').throws(); + barPlugin = { + exportName: 'bar', + validate: sinon.stub(), + finalize: impls => impls.map(() => 'BAR') + }; + bazPlugin = { + exportName: 'baz' + }; + pluginLoader = PluginLoader.create([fooPlugin, barPlugin, bazPlugin]); + }); + + describe('when no plugins have been loaded', function() { + it('should return an empty map', async function() { + return expect(pluginLoader.finalize(), 'to be fulfilled with', {}); + }); + }); + + describe('when a plugin has one or more implementations', function() { + beforeEach(function() { + pluginLoader.load({foo: sinon.stub()}); + pluginLoader.load({foo: sinon.stub()}); + }); + + it('should return an object map using `optionName` key for each registered plugin', async function() { + return expect(pluginLoader.finalize(), 'to be fulfilled with', { + fooOption: ['FOO', 'FOO'] + }); + }); + + it('should omit unused plugins', async function() { + pluginLoader.load({bar: sinon.stub()}); + return expect(pluginLoader.finalize(), 'to be fulfilled with', { + fooOption: ['FOO', 'FOO'], + bar: ['BAR'] + }); + }); + }); + + describe('when a plugin has no "finalize" function', function() { + it('should return an array of raw implementations', function() { + pluginLoader.load({baz: 'polar bears'}); + return expect(pluginLoader.finalize(), 'to be fulfilled with', { + baz: ['polar bears'] + }); + }); + }); + }); + }); + }); + + describe('root hoots plugin', function() { + let pluginLoader; + + beforeEach(function() { + pluginLoader = PluginLoader.create(); + }); + + describe('when impl is an array', function() { + it('should fail validation', function() { + expect(() => pluginLoader.load({mochaHooks: []}), 'to throw', { + code: INVALID_PLUGIN_IMPLEMENTATION + }); + }); + }); + + describe('when impl is a primitive', function() { + it('should fail validation', function() { + expect(() => pluginLoader.load({mochaHooks: 'nuts'}), 'to throw', { + code: INVALID_PLUGIN_IMPLEMENTATION + }); + }); + }); + + describe('when impl is a function', function() { + it('should pass validation', function() { + expect(pluginLoader.load({mochaHooks: sinon.stub()}), 'to be true'); + }); + }); + + describe('when impl is an object of functions', function() { + // todo: hook name validation? + it('should pass validation'); + }); + + describe('when a loaded impl is finalized', function() { + it('should flatten the implementations', async function() { + function a() {} + function b() {} + function d() {} + function g() {} + async function f() {} + function c() { + return { + beforeAll: d, + beforeEach: g + }; + } + async function e() { + return { + afterEach: f + }; + } + + [ + { + beforeEach: a + }, + { + afterAll: b + }, + c, + e + ].forEach(impl => { + pluginLoader.load({mochaHooks: impl}); + }); + + return expect(pluginLoader.finalize(), 'to be fulfilled with', { + rootHooks: { + beforeAll: [d], + beforeEach: [a, g], + afterAll: [b], + afterEach: [f] + } + }); + }); + }); + }); + + describe('global fixtures plugin', function() { + let pluginLoader; + + beforeEach(function() { + pluginLoader = PluginLoader.create(); + }); + + describe('global setup', function() { + describe('when an implementation is a primitive', function() { + it('should fail validation', function() { + expect( + () => pluginLoader.load({mochaGlobalSetup: 'nuts'}), + 'to throw' + ); + }); + }); + describe('when an implementation is an array of primitives', function() { + it('should fail validation', function() { + expect( + () => pluginLoader.load({mochaGlobalSetup: ['nuts']}), + 'to throw' + ); + }); + }); + + describe('when an implementation is a function', function() { + it('should pass validation', function() { + expect( + pluginLoader.load({mochaGlobalSetup: sinon.stub()}), + 'to be true' + ); + }); + }); + + describe('when an implementation is an array of functions', function() { + it('should pass validation', function() { + expect( + pluginLoader.load({mochaGlobalSetup: [sinon.stub()]}), + 'to be true' + ); + }); + }); + }); + + describe('global teardown', function() { + describe('when an implementation is a primitive', function() { + it('should fail validation', function() { + expect( + () => pluginLoader.load({mochaGlobalTeardown: 'nuts'}), + 'to throw' + ); + }); + }); + describe('when an implementation is an array of primitives', function() { + it('should fail validation', function() { + expect( + () => pluginLoader.load({mochaGlobalTeardown: ['nuts']}), + 'to throw' + ); + }); + }); + + describe('when an implementation is a function', function() { + it('should pass validation', function() { + expect( + pluginLoader.load({mochaGlobalTeardown: sinon.stub()}), + 'to be true' + ); + }); + }); + + describe('when an implementation is an array of functions', function() { + it('should pass validation', function() { + expect( + pluginLoader.load({mochaGlobalTeardown: [sinon.stub()]}), + 'to be true' + ); + }); + }); + }); + }); +}); diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js index 730c40003d..911d7d1d86 100644 --- a/test/unit/runner.spec.js +++ b/test/unit/runner.spec.js @@ -569,10 +569,6 @@ describe('Runner', function() { ); done(); }); - - afterEach(function() { - runner.dispose(); - }); }); describe('.dispose', function() {