From 576eb6027f86f31b262a42d61e9d6a1fc1a1884c Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Sat, 7 Jul 2018 00:54:11 -0400 Subject: [PATCH] refactor: simplify plugin validation --- lib/definitions/errors.js | 24 ++--- lib/definitions/plugins.js | 47 +++------ lib/plugins/index.js | 19 ++-- lib/plugins/normalize.js | 18 ++-- test/definitions/plugins.test.js | 161 ++++++++++++++----------------- test/index.test.js | 3 - test/plugins/normalize.test.js | 12 ++- 7 files changed, 125 insertions(+), 159 deletions(-) diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 232c4c2d..f5aa40bc 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -4,7 +4,7 @@ const {toLower, isString} = require('lodash'); const pkg = require('../../package.json'); const {RELEASE_TYPE} = require('./constants'); -const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}}); +const homepage = url.format({...url.parse(pkg.homepage), hash: null}); const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5})); const linkify = file => `${homepage}/blob/caribou/${file}`; @@ -55,25 +55,25 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.` Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`, }), - EPLUGINCONF: ({pluginType, pluginConf}) => ({ - message: `The \`${pluginType}\` plugin configuration is invalid.`, - details: `The [${pluginType} plugin configuration](${linkify( - `docs/usage/plugins.md#${toLower(pluginType)}-plugin` + EPLUGINCONF: ({type, pluginConf}) => ({ + message: `The \`${type}\` plugin configuration is invalid.`, + details: `The [${type} plugin configuration](${linkify( + `docs/usage/plugins.md#${toLower(type)}-plugin` )}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property. - Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`, + Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`, }), - EPLUGIN: ({pluginName, pluginType}) => ({ - message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`, - details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`. + EPLUGIN: ({pluginName, type}) => ({ + message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`, + details: `A valid \`${type}\` **semantic-release** plugin must be a function or an object with a function in the property \`${type}\`. -The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step. +The plugin \`${pluginName}\` doesn't have the property \`${type}\` and cannot be used for the \`${type}\` step. Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify( 'docs/usage/plugins.md' )}) documentation for more details.`, }), - EANALYZEOUTPUT: ({result, pluginName}) => ({ + EANALYZECOMMITSOUTPUT: ({result, pluginName}) => ({ message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.', details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map( type => `\`${type}\`` @@ -89,7 +89,7 @@ We recommend to report the issue to the \`${pluginName}\` authors, providing the 'docs/developer-guide/plugin.md' )})`, }), - ERELEASENOTESOUTPUT: ({result, pluginName}) => ({ + EGENERATENOTESOUTPUT: ({result, pluginName}) => ({ message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.', details: `The \`generateNotes\` plugin must return a \`String\`. diff --git a/lib/definitions/plugins.js b/lib/definitions/plugins.js index 2fdf3378..62c33150 100644 --- a/lib/definitions/plugins.js +++ b/lib/definitions/plugins.js @@ -6,62 +6,37 @@ const validatePluginConfig = conf => isString(conf) || isString(conf.path) || is module.exports = { verifyConditions: { default: ['@semantic-release/npm', '@semantic-release/github'], - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), }, analyzeCommits: { default: '@semantic-release/commit-analyzer', - config: { - validator: conf => Boolean(conf) && validatePluginConfig(conf), - }, - output: { - validator: output => !output || RELEASE_TYPE.includes(output), - error: 'EANALYZEOUTPUT', - }, + configValidator: conf => Boolean(conf) && validatePluginConfig(conf), + outputValidator: output => !output || RELEASE_TYPE.includes(output), }, verifyRelease: { default: false, - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), }, generateNotes: { default: '@semantic-release/release-notes-generator', - config: { - validator: conf => !conf || validatePluginConfig(conf), - }, - output: { - validator: output => !output || isString(output), - error: 'ERELEASENOTESOUTPUT', - }, + configValidator: conf => !conf || validatePluginConfig(conf), + outputValidator: output => !output || isString(output), }, prepare: { default: ['@semantic-release/npm'], - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), }, publish: { default: ['@semantic-release/npm', '@semantic-release/github'], - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, - output: { - validator: output => !output || isPlainObject(output), - error: 'EPUBLISHOUTPUT', - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), + outputValidator: output => !output || isPlainObject(output), }, success: { default: ['@semantic-release/github'], - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), }, fail: { default: ['@semantic-release/github'], - config: { - validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), - }, + configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), }, }; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 877dcdd6..1f57ac81 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -7,28 +7,27 @@ const normalize = require('./normalize'); module.exports = (options, pluginsPath, logger) => { const errors = []; - const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => { - const {config, default: def} = PLUGINS_DEFINITIONS[pluginType]; + const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce((plugins, [type, {configValidator, default: def}]) => { let pluginConfs; - if (isUndefined(options[pluginType])) { + if (isUndefined(options[type])) { pluginConfs = def; } else { // If an object is passed and the path is missing, set the default one for single plugins - if (isPlainObject(options[pluginType]) && !options[pluginType].path && castArray(def).length === 1) { - options[pluginType].path = def; + if (isPlainObject(options[type]) && !options[type].path && castArray(def).length === 1) { + options[type].path = def; } - if (config && !config.validator(options[pluginType])) { - errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]})); + if (configValidator && !configValidator(options[type])) { + errors.push(getError('EPLUGINCONF', {type, pluginConf: options[type]})); return plugins; } - pluginConfs = options[pluginType]; + pluginConfs = options[type]; } const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS)); - plugins[pluginType] = pipeline( - castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger)) + plugins[type] = pipeline( + castArray(pluginConfs).map(conf => normalize(type, pluginsPath, globalOpts, conf, logger)) ); return plugins; diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js index 7745d1ef..0fac87df 100644 --- a/lib/plugins/normalize.js +++ b/lib/plugins/normalize.js @@ -7,7 +7,7 @@ const PLUGINS_DEFINITIONS = require('../definitions/plugins'); /* eslint max-params: ["error", 5] */ -module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => { +module.exports = (type, pluginsPath, globalOpts, pluginOpts, logger) => { if (!pluginOpts) { return noop; } @@ -17,9 +17,9 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => { if (!isFunction(pluginOpts)) { if (pluginsPath[path]) { - logger.log('Load plugin "%s" from %s in shareable config %s', pluginType, path, pluginsPath[path]); + logger.log('Load plugin "%s" from %s in shareable config %s', type, path, pluginsPath[path]); } else { - logger.log('Load plugin "%s" from %s', pluginType, path); + logger.log('Load plugin "%s" from %s', type, path); } } @@ -33,18 +33,18 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => { let func; if (isFunction(plugin)) { func = plugin.bind(null, cloneDeep({...globalOpts, ...config})); - } else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) { - func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config})); + } else if (isPlainObject(plugin) && plugin[type] && isFunction(plugin[type])) { + func = plugin[type].bind(null, cloneDeep({...globalOpts, ...config})); } else { - throw getError('EPLUGIN', {pluginType, pluginName}); + throw getError('EPLUGIN', {type, pluginName}); } const validator = async input => { - const definition = PLUGINS_DEFINITIONS[pluginType]; + const {outputValidator} = PLUGINS_DEFINITIONS[type] || {}; try { const result = await func(cloneDeep(input)); - if (definition && definition.output && !definition.output.validator(result)) { - throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName}); + if (outputValidator && !outputValidator(result)) { + throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName}); } return result; } catch (err) { diff --git a/test/definitions/plugins.test.js b/test/definitions/plugins.test.js index f016c59a..5703be5b 100644 --- a/test/definitions/plugins.test.js +++ b/test/definitions/plugins.test.js @@ -1,133 +1,120 @@ import test from 'ava'; import plugins from '../../lib/definitions/plugins'; -import errors from '../../lib/definitions/errors'; test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => { - t.false(plugins.verifyConditions.config.validator({})); - t.false(plugins.verifyConditions.config.validator({path: null})); + t.false(plugins.verifyConditions.configValidator({})); + t.false(plugins.verifyConditions.configValidator({path: null})); - t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'})); - t.true(plugins.verifyConditions.config.validator()); - t.true(plugins.verifyConditions.config.validator('plugin-path.js')); - t.true(plugins.verifyConditions.config.validator(() => {})); - t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.verifyConditions.configValidator({path: 'plugin-path.js'})); + t.true(plugins.verifyConditions.configValidator()); + t.true(plugins.verifyConditions.configValidator('plugin-path.js')); + t.true(plugins.verifyConditions.configValidator(() => {})); + t.true(plugins.verifyConditions.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => { - t.false(plugins.analyzeCommits.config.validator({})); - t.false(plugins.analyzeCommits.config.validator({path: null})); - t.false(plugins.analyzeCommits.config.validator([])); - t.false(plugins.analyzeCommits.config.validator()); + t.false(plugins.analyzeCommits.configValidator({})); + t.false(plugins.analyzeCommits.configValidator({path: null})); + t.false(plugins.analyzeCommits.configValidator([])); + t.false(plugins.analyzeCommits.configValidator()); - t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'})); - t.true(plugins.analyzeCommits.config.validator('plugin-path.js')); - t.true(plugins.analyzeCommits.config.validator(() => {})); + t.true(plugins.analyzeCommits.configValidator({path: 'plugin-path.js'})); + t.true(plugins.analyzeCommits.configValidator('plugin-path.js')); + t.true(plugins.analyzeCommits.configValidator(() => {})); }); test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => { - t.false(plugins.verifyRelease.config.validator({})); - t.false(plugins.verifyRelease.config.validator({path: null})); + t.false(plugins.verifyRelease.configValidator({})); + t.false(plugins.verifyRelease.configValidator({path: null})); - t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'})); - t.true(plugins.verifyRelease.config.validator()); - t.true(plugins.verifyRelease.config.validator('plugin-path.js')); - t.true(plugins.verifyRelease.config.validator(() => {})); - t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'})); + t.true(plugins.verifyRelease.configValidator()); + t.true(plugins.verifyRelease.configValidator('plugin-path.js')); + t.true(plugins.verifyRelease.configValidator(() => {})); + t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => { - t.false(plugins.generateNotes.config.validator({})); - t.false(plugins.generateNotes.config.validator({path: null})); - t.false(plugins.generateNotes.config.validator([])); + t.false(plugins.generateNotes.configValidator({})); + t.false(plugins.generateNotes.configValidator({path: null})); + t.false(plugins.generateNotes.configValidator([])); - t.true(plugins.generateNotes.config.validator()); - t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'})); - t.true(plugins.generateNotes.config.validator('plugin-path.js')); - t.true(plugins.generateNotes.config.validator(() => {})); + t.true(plugins.generateNotes.configValidator()); + t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'})); + t.true(plugins.generateNotes.configValidator('plugin-path.js')); + t.true(plugins.generateNotes.configValidator(() => {})); }); test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => { - t.false(plugins.verifyRelease.config.validator({})); - t.false(plugins.verifyRelease.config.validator({path: null})); + t.false(plugins.verifyRelease.configValidator({})); + t.false(plugins.verifyRelease.configValidator({path: null})); - t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'})); - t.true(plugins.verifyRelease.config.validator()); - t.true(plugins.verifyRelease.config.validator('plugin-path.js')); - t.true(plugins.verifyRelease.config.validator(() => {})); - t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'})); + t.true(plugins.verifyRelease.configValidator()); + t.true(plugins.verifyRelease.configValidator('plugin-path.js')); + t.true(plugins.verifyRelease.configValidator(() => {})); + t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => { - t.false(plugins.publish.config.validator({})); - t.false(plugins.publish.config.validator({path: null})); + t.false(plugins.publish.configValidator({})); + t.false(plugins.publish.configValidator({path: null})); - t.true(plugins.publish.config.validator({path: 'plugin-path.js'})); - t.true(plugins.publish.config.validator()); - t.true(plugins.publish.config.validator('plugin-path.js')); - t.true(plugins.publish.config.validator(() => {})); - t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.publish.configValidator({path: 'plugin-path.js'})); + t.true(plugins.publish.configValidator()); + t.true(plugins.publish.configValidator('plugin-path.js')); + t.true(plugins.publish.configValidator(() => {})); + t.true(plugins.publish.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => { - t.false(plugins.success.config.validator({})); - t.false(plugins.success.config.validator({path: null})); + t.false(plugins.success.configValidator({})); + t.false(plugins.success.configValidator({path: null})); - t.true(plugins.success.config.validator({path: 'plugin-path.js'})); - t.true(plugins.success.config.validator()); - t.true(plugins.success.config.validator('plugin-path.js')); - t.true(plugins.success.config.validator(() => {})); - t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.success.configValidator({path: 'plugin-path.js'})); + t.true(plugins.success.configValidator()); + t.true(plugins.success.configValidator('plugin-path.js')); + t.true(plugins.success.configValidator(() => {})); + t.true(plugins.success.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => { - t.false(plugins.fail.config.validator({})); - t.false(plugins.fail.config.validator({path: null})); + t.false(plugins.fail.configValidator({})); + t.false(plugins.fail.configValidator({path: null})); - t.true(plugins.fail.config.validator({path: 'plugin-path.js'})); - t.true(plugins.fail.config.validator()); - t.true(plugins.fail.config.validator('plugin-path.js')); - t.true(plugins.fail.config.validator(() => {})); - t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); + t.true(plugins.fail.configValidator({path: 'plugin-path.js'})); + t.true(plugins.fail.configValidator()); + t.true(plugins.fail.configValidator('plugin-path.js')); + t.true(plugins.fail.configValidator(() => {})); + t.true(plugins.fail.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); }); test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => { - t.false(plugins.analyzeCommits.output.validator('invalid')); - t.false(plugins.analyzeCommits.output.validator(1)); - t.false(plugins.analyzeCommits.output.validator({})); + t.false(plugins.analyzeCommits.outputValidator('invalid')); + t.false(plugins.analyzeCommits.outputValidator(1)); + t.false(plugins.analyzeCommits.outputValidator({})); - t.true(plugins.analyzeCommits.output.validator()); - t.true(plugins.analyzeCommits.output.validator(null)); - t.true(plugins.analyzeCommits.output.validator('major')); + t.true(plugins.analyzeCommits.outputValidator()); + t.true(plugins.analyzeCommits.outputValidator(null)); + t.true(plugins.analyzeCommits.outputValidator('major')); }); test('The "generateNotes" plugin output, if defined, must be a string', t => { - t.false(plugins.generateNotes.output.validator(1)); - t.false(plugins.generateNotes.output.validator({})); + t.false(plugins.generateNotes.outputValidator(1)); + t.false(plugins.generateNotes.outputValidator({})); - t.true(plugins.generateNotes.output.validator()); - t.true(plugins.generateNotes.output.validator(null)); - t.true(plugins.generateNotes.output.validator('')); - t.true(plugins.generateNotes.output.validator('string')); + t.true(plugins.generateNotes.outputValidator()); + t.true(plugins.generateNotes.outputValidator(null)); + t.true(plugins.generateNotes.outputValidator('')); + t.true(plugins.generateNotes.outputValidator('string')); }); test('The "publish" plugin output, if defined, must be an object', t => { - t.false(plugins.publish.output.validator(1)); - t.false(plugins.publish.output.validator('string')); + t.false(plugins.publish.outputValidator(1)); + t.false(plugins.publish.outputValidator('string')); - t.true(plugins.publish.output.validator({})); - t.true(plugins.publish.output.validator()); - t.true(plugins.publish.output.validator(null)); - t.true(plugins.publish.output.validator('')); -}); - -test('The "analyzeCommits" plugin output definition return an existing error code', t => { - t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error)); -}); - -test('The "generateNotes" plugin output definition return an existing error code', t => { - t.true(Object.keys(errors).includes(plugins.generateNotes.output.error)); -}); - -test('The "publish" plugin output definition return an existing error code', t => { - t.true(Object.keys(errors).includes(plugins.publish.output.error)); + t.true(plugins.publish.outputValidator({})); + t.true(plugins.publish.outputValidator()); + t.true(plugins.publish.outputValidator(null)); + t.true(plugins.publish.outputValidator('')); }); diff --git a/test/index.test.js b/test/index.test.js index 22cc0cd2..ccab7cb4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -4,7 +4,6 @@ import {spy, stub} from 'sinon'; import clearModule from 'clear-module'; import AggregateError from 'aggregate-error'; import SemanticReleaseError from '@semantic-release/error'; -import DEFINITIONS from '../lib/definitions/plugins'; import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants'; import { gitHead as getGitHead, @@ -931,8 +930,6 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => { }); const error = await t.throws(semanticRelease(options), Error); - // Verify error message - t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message)); t.regex(error.details, /string/); }); diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js index 4b26c009..d8ab789d 100644 --- a/test/plugins/normalize.test.js +++ b/test/plugins/normalize.test.js @@ -94,8 +94,10 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the const error = await t.throws(plugin()); - t.is(error.code, 'EANALYZEOUTPUT'); + t.is(error.code, 'EANALYZECOMMITSOUTPUT'); t.is(error.name, 'SemanticReleaseError'); + t.truthy(error.message); + t.truthy(error.details); t.regex(error.details, /2/); }); @@ -105,8 +107,10 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the const error = await t.throws(plugin()); - t.is(error.code, 'ERELEASENOTESOUTPUT'); + t.is(error.code, 'EGENERATENOTESOUTPUT'); t.is(error.name, 'SemanticReleaseError'); + t.truthy(error.message); + t.truthy(error.details); t.regex(error.details, /2/); }); @@ -123,6 +127,8 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin t.is(error.code, 'EPUBLISHOUTPUT'); t.is(error.name, 'SemanticReleaseError'); + t.truthy(error.message); + t.truthy(error.details); t.regex(error.details, /2/); }); @@ -187,6 +193,8 @@ test('Throws an error if the plugin return an object without the expected plugin t.is(error.code, 'EPLUGIN'); t.is(error.name, 'SemanticReleaseError'); + t.truthy(error.message); + t.truthy(error.details); }); test('Throws an error if the plugin is not found', t => {