ℹ️ Added in v12.5.0
Cucumber's functionality can be extended with plugins.
You can specify one or more plugins via the plugin configuration option:
- In a configuration file
{ plugin: ['@my-org/some-plugin', '@my-org/other-plugin'] } - On the CLI
cucumber-js --plugin @my-org/some-plugin --plugin @my-org/other-plugin"
The above examples make sense if you're using a plugin installed from a package registry like npm. If you have a custom plugin (see below) that's local to your project, the specifier might be something like ./my-plugin.js.
You can also specify options as JSON, if a plugin needs them:
- In a configuration file
{ pluginOptions: { someOption: true } } - On the CLI
cucumber-js --plugin-options '{"someOption":true}'
You can write your own plugins. They allow you to:
- Listen to Cucumber's message stream
- Hook into core behaviour and change things
- Output extra information for the user
The guide below should help you get a skeletal plugin up and running, and you can find full API and type documentation at:
https://cucumber.github.io/cucumber-js/modules/api.html
Also, some of Cucumber's built-in functionality is implemented as plugins, which you might find useful for examples:
A plugin in its simplest form is an object identifying itself as a plugin, with a "coordinator" function to be run. It should be the default export of your plugin package or module. Here's a minimal plugin skeleton:
export default {
type: 'plugin',
coordinator: (context) => {
// do stuff here
}
}(That example is an ES Module, but CommonJS works just as well.)
Your plugin is initialised early in Cucumber's lifecycle, just after we have resolved the configuration. You can do async setup work in your plugin function.
Your plugin is stopped after the test run finishes, just before Cucumber exits. If you need to do cleanup work, your plugin function can return a cleanup function - it will be executed before Cucumber exits, and again can be async.
Here's a rough example:
export default {
type: 'plugin',
coordinator: async (context) => {
// this runs at the start
await doSomeAsyncSetup()
return async () => {
// this runs at the end
await doSomeAsyncTeardown()
}
}
}A plugin function accepts a single argument which provides the context for your plugin. It has:
operation- the Cucumber operation being runon(event: string, handler: Function)- function for registering an event handlertransform(event: string, handler: Function)- function for registering a transformoptions- options provided by the user in their configurationlogger- a minimal logger, directed tostderrenvironment- attributes of the current environment
You can register passive event handlers for these things that happen in Cucumber:
message- emitted for each Cucumber Message during the process. These are most commonly consumed by Formatters, but have other uses too.paths:resolve- emitted when Cucumber has resolved the paths on the file system from which it will load feature files and support code.
Here's an example emitting a log when the test run finishes:
export default {
type: 'plugin',
coordinator: ({
on,
logger
}) => {
on('message', envelope => {
if (envelope.testRunFinished) {
logger.info('Test run finished!')
}
})
}
}You can register transforms for some things that happen in Cucumber in order to modify or augment the built-in behaviour. Your transformer function can be async and should treat the input value as immutable, returning a new value that reflects your modifications.
These are the operations for which you can register transforms:
pickles:filter- called when Cucumber has compiled Pickles for all found Scenarios, and now needs to determine which ones should be run as test cases. Return a new array after doing your filtering.pickles:order- called after Cucumber has filtered Pickles, so it can sort them. This works the same as the one above.
Here's an example filtering off some unwanted Pickles:
export default {
type: 'plugin',
coordinator: ({
on,
logger
}) => {
transform('pickles:filter', pickles => {
return pickles.filter(({pickle}) => !pickle.name.includes('widgets'))
})
}
}Users provide a single pluginOptions object to Cucumber via their configuration. If you want to target a specific key within this object, you can do that with the optionsKey prop on your plugin object, and Cucumber will only give you that block:
export default {
type: 'plugin',
coordinator: ({ options, on, logger }) => {
on('message', (message) => {
if (message.testRunFinished) {
logger.info(options.bar) // yields `2` if the options are `{foo: {bar: 2}}`
}
})
},
optionsKey: 'foo'
}The logger in your context object can be used to direct user-facing messages to stderr. Use it sparingly - users don't enjoy a lot of noise in their terminal. If it's for diagnostics or troubleshooting, consider using the debug method, so it's opt-in for users.
If your plugin throws an error during its initialisation, cleanup, event handlers or transforms, Cucumber will wrap it with some contextual metadata but otherwise allow it to bubble and cause the test run to fail and exit with a non-zero code. If something your plugin does could yield an error that doesn't constitute a total failure, you should catch and handle it inside of your plugin.
If you're using TypeScript, you can get full type checking and completion, including for event handlers, transforms and options, using the Plugin type:
import type { Plugin } from '@cucumber/cucumber/api'
type MyPluginOptions = {
foo: {
bar: number
}
}
export default myPlugin: Plugin<MyOptions> = {
type: 'plugin',
coordinator: (context) => {...},
optionsKey: 'foo'
}