diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index d30a111e60..4e163db27f 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -13,9 +13,9 @@ const debug = require('debug')('mocha:cli:run:helpers'); const {watchRun, watchParallelRun} = require('./watch-run'); const collectFiles = require('./collect-files'); const {format} = require('util'); -const {createInvalidPluginError} = require('../errors'); +const {createInvalidLegacyPluginError} = require('../errors'); const {requireOrImport} = require('../esm-utils'); -const {PluginLoader} = require('../plugin'); +const PluginLoader = require('../plugin-loader'); /** * Exits Mocha when tests + code under test has finished execution (default) @@ -79,7 +79,7 @@ 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 = []) => { @@ -93,11 +93,17 @@ exports.handleRequires = async (requires = []) => { } const requiredModule = await requireOrImport(modpath); if (requiredModule && typeof requiredModule === 'object') { - pluginLoader.load(requiredModule); + if (pluginLoader.load(requiredModule)) { + debug('found one or more plugin implementations in %s', modpath); + } } debug('loaded required module "%s"', mod); } - return pluginLoader.finalize(); + const plugins = await pluginLoader.finalize(); + if (Object.keys(plugins).length) { + debug('finalized plugin implementations: %O', plugins); + } + return plugins; }; /** @@ -208,14 +214,14 @@ exports.validateLegacyPlugin = (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 @@ -231,10 +237,10 @@ exports.validateLegacyPlugin = (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/errors.js b/lib/errors.js index 929f56399c..796c7e0b10 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 8d1a6f418e..f56356d0d7 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -1280,3 +1280,30 @@ Mocha.prototype.hasGlobalTeardownFixtures = function hasGlobalTeardownFixtures() * @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/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/plugin.js b/lib/plugin.js deleted file mode 100644 index 60b267b9e5..0000000000 --- a/lib/plugin.js +++ /dev/null @@ -1,204 +0,0 @@ -// @ts-check - -/** - * Provides a way to load "plugins" as provided by the user. - * - * Currently supports: - * - * - Root hooks - * - Global fixtures (setup/teardown) - * - * @module plugin - * @private - */ - -'use strict'; - -const {createUnsupportedError} = require('./errors'); -const {castArray} = require('./utils'); - -const createFunctionArrayValidator = pluginType => 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 createUnsupportedError( - `${pluginType} must be a function or an array of functions` - ); - } -}; - -/** - * @type {Set} - */ -const MochaPlugins = new Set([ - { - exportName: 'mochaHooks', - optionName: 'rootHooks', - validate(value) { - if ( - Array.isArray(value) || - (typeof value !== 'function' && typeof value !== 'object') - ) { - throw createUnsupportedError( - `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: []} - ); - } - } - }, - { - exportName: 'mochaGlobalSetup', - optionName: 'globalSetup', - validate: createFunctionArrayValidator('mochaGlobalSetup') - }, - { - exportName: 'mochaGlobalTeardown', - optionName: 'globalTeardown', - validate: createFunctionArrayValidator('mochaGlobalTeardown') - } -]); - -/** - * Loads plugins; {@link PluginLoader#load} should be called for all required - * modules, and finally the result of {@link PluginLoader#finalize} should be - * handed as options to the `Mocha` constructor. - */ -exports.PluginLoader = class PluginLoader { - /** - * Initializes plugin names, plugin map, etc. - * @param {PluginDefinition[]|Set} pluginDefinitions - Plugin definitions - */ - constructor(pluginDefinitions = MochaPlugins) { - this.registered = new Map(); - - pluginDefinitions.forEach(pluginDef => { - this.register(pluginDef); - }); - - this.loaded = new Map(this.exportNames.map(name => [name, []])); - } - - register(plugin) { - if (this.registered.has(plugin.exportName)) { - throw new Error(`plugin name conflict: ${plugin.exportName}`); - } - this.registered.set(plugin.exportName, plugin); - } - - get exportNames() { - return Array.from(this.registered).map(([key]) => key); - } - - 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 this.exportNames.reduce((isFound, pluginName) => { - const impl = requiredModule[pluginName]; - if (impl) { - const plugin = this.registered.get(pluginName); - if (plugin.validate) { - plugin.validate(impl); - } - this.loaded.set(pluginName, [ - ...this.loaded.get(pluginName), - ...castArray(impl) - ]); - return true; - } - return isFound; - }, false); - } - return false; - } - - async finalize() { - const finalizedPlugins = Object.create(null); - - for await (const [exportName, impls] of this.loaded.entries()) { - if (impls.length) { - const plugin = this.registered.get(exportName); - if (typeof plugin.finalize === 'function') { - finalizedPlugins[ - plugin.optionName || exportName - ] = await plugin.finalize(impls); - } - } - } - - return finalizedPlugins; - } - - /** - * Constructs a {@link PluginLoader} - * @param {PluginDefinition[]|Set} [pluginDefs] - Plugin definitions - */ - static create(pluginDefs = MochaPlugins) { - return new PluginLoader(pluginDefs); - } -}; - -/** - * 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 map of constants to (typically) `string` values. - * - * **Not** a `Map`. - * @template T - * @typedef {{[key: string]: T}} ConstantMap - * @readonly - */ - -/** - * A function to validate a user-supplied plugin implementation - * @callback PluginValidator - * @param {*} value - Value to check - * @returns {void} - */ - -/** - * A function to finalize plugins impls of a particular ilk - * @callback PluginFinalizer - * @param {Array<*>} impls - User-supplied implementations - * @returns {Promise<*>|*} - */ diff --git a/test/node-unit/worker.spec.js b/test/node-unit/worker.spec.js index 9b5a6a8738..4081708905 100644 --- a/test/node-unit/worker.spec.js +++ b/test/node-unit/worker.spec.js @@ -67,7 +67,7 @@ describe('worker', function() { '../../lib/mocha': stubs.Mocha, '../../lib/nodejs/serializer': stubs.serializer, '../../lib/cli/run-helpers': stubs.runHelpers, - '../../lib/plugin': stubs.plugin + '../../lib/plugin-loader': stubs.plugin }); }); diff --git a/test/node-unit/plugin.spec.js b/test/unit/plugin-loader.spec.js similarity index 86% rename from test/node-unit/plugin.spec.js rename to test/unit/plugin-loader.spec.js index 31f9556db3..b72bee617a 100644 --- a/test/node-unit/plugin.spec.js +++ b/test/unit/plugin-loader.spec.js @@ -1,19 +1,14 @@ 'use strict'; -const rewiremock = require('rewiremock/node'); +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() { - /** - * @type {import('../../lib/plugin').PluginLoader} - */ - let PluginLoader; - - beforeEach(function() { - PluginLoader = rewiremock.proxy('../../lib/plugin', {}).PluginLoader; - }); - describe('constructor', function() { describe('when passed no options', function() { it('should populate a registry of built-in plugins', function() { @@ -60,13 +55,43 @@ describe('plugin module', function() { describe('when the plugin export name is already in use', function() { it('should throw', function() { - expect( - () => pluginLoader.register({exportName: 'mochaHooks'}), - 'to throw' - ); + 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; @@ -218,6 +243,7 @@ describe('plugin module', function() { let pluginLoader; let fooPlugin; let barPlugin; + let bazPlugin; beforeEach(function() { fooPlugin = { @@ -232,7 +258,10 @@ describe('plugin module', function() { validate: sinon.stub(), finalize: impls => impls.map(() => 'BAR') }; - pluginLoader = PluginLoader.create([fooPlugin, barPlugin]); + bazPlugin = { + exportName: 'baz' + }; + pluginLoader = PluginLoader.create([fooPlugin, barPlugin, bazPlugin]); }); describe('when no plugins have been loaded', function() { @@ -261,32 +290,39 @@ describe('plugin module', function() { }); }); }); + + 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() { - /** - * @type {import('../../lib/plugin').PluginLoader} - */ - let PluginLoader; - let pluginLoader; beforeEach(function() { - PluginLoader = rewiremock.proxy('../../lib/plugin', {}).PluginLoader; pluginLoader = PluginLoader.create(); }); describe('when impl is an array', function() { it('should fail validation', function() { - expect(() => pluginLoader.load({mochaHooks: []}), 'to throw'); + 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'); + expect(() => pluginLoader.load({mochaHooks: 'nuts'}), 'to throw', { + code: INVALID_PLUGIN_IMPLEMENTATION + }); }); }); @@ -346,15 +382,9 @@ describe('plugin module', function() { }); describe('global fixtures plugin', function() { - /** - * @type {import('../../lib/plugin').PluginLoader} - */ - let PluginLoader; - let pluginLoader; beforeEach(function() { - PluginLoader = rewiremock.proxy('../../lib/plugin', {}).PluginLoader; pluginLoader = PluginLoader.create(); });