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() {