From 63ab911a9d72d968018ff8ee394a12eb2b5f5011 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 15:53:14 +0100 Subject: [PATCH 1/7] WIP --- src/api/formatters.ts | 38 ------------------ src/api/plugins.ts | 17 ++++++++ src/api/run_cucumber.ts | 6 +++ src/formatter/index.ts | 7 +--- src/plugin/index.ts | 2 + src/plugin/plugin_manager.ts | 37 +++++++++++++++++ src/plugin/types.ts | 12 ++++++ src/{formatter => publish}/http_stream.ts | 0 src/publish/index.ts | 3 ++ src/publish/publish_plugin.ts | 49 +++++++++++++++++++++++ 10 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 src/api/plugins.ts create mode 100644 src/plugin/index.ts create mode 100644 src/plugin/plugin_manager.ts create mode 100644 src/plugin/types.ts rename src/{formatter => publish}/http_stream.ts (100%) create mode 100644 src/publish/index.ts create mode 100644 src/publish/publish_plugin.ts diff --git a/src/api/formatters.ts b/src/api/formatters.ts index ab83c05cb..c999aa74b 100644 --- a/src/api/formatters.ts +++ b/src/api/formatters.ts @@ -7,19 +7,12 @@ import { WriteStream as TtyWriteStream } from 'tty' import FormatterBuilder from '../formatter/builder' import fs from 'mz/fs' import path from 'path' -import { DEFAULT_CUCUMBER_PUBLISH_URL } from '../formatter/publish' -import HttpStream from '../formatter/http_stream' -import { Writable } from 'stream' -import { supportsColor } from 'supports-color' import { IRunOptionsFormats } from './types' -import hasAnsi from 'has-ansi' -import stripAnsi from 'strip-ansi' export async function initializeFormatters({ env, cwd, stdout, - stderr, logger, onStreamError, eventBroadcaster, @@ -83,38 +76,7 @@ export async function initializeFormatters({ formatters.push(await initializeFormatter(stream, target, type)) } - if (configuration.publish) { - const { url = DEFAULT_CUCUMBER_PUBLISH_URL, token } = configuration.publish - const headers: { [key: string]: string } = {} - if (token !== undefined) { - headers.Authorization = `Bearer ${token}` - } - const stream = new HttpStream(url, 'GET', headers) - const readerStream = new Writable({ - objectMode: true, - write: function (responseBody: string, encoding, writeCallback) { - logger.error(sanitisePublishOutput(responseBody, stderr)) - writeCallback() - }, - }) - stream.pipe(readerStream) - formatters.push(await initializeFormatter(stream, url, 'message')) - } - return async function () { await Promise.all(formatters.map(async (f) => await f.finished())) } } - -/* -This is because the Cucumber Reports service returns a pre-formatted console message -including ANSI escapes, so if our stderr stream doesn't support those we need to -strip them back out. Ideally we should get structured data from the service and -compose the console message on this end. - */ -function sanitisePublishOutput(raw: string, stderr: IFormatterStream) { - if (!supportsColor(stderr) && hasAnsi(raw)) { - return stripAnsi(raw) - } - return raw -} diff --git a/src/api/plugins.ts b/src/api/plugins.ts new file mode 100644 index 000000000..0e4be9593 --- /dev/null +++ b/src/api/plugins.ts @@ -0,0 +1,17 @@ +import { Plugin, PluginManager } from '../plugin' +import publishPlugin from '../publish' +import { IRunEnvironment, IRunOptions } from './types' + +const INTERNAL_PLUGINS: Record = { + publish: publishPlugin, +} + +export async function initializePlugins( + configuration: IRunOptions, + environment: IRunEnvironment +): Promise { + // eventually we'll load plugin packages here + const pluginManager = new PluginManager(Object.values(INTERNAL_PLUGINS)) + await pluginManager.init(configuration, environment) + return pluginManager +} diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index 05791acbb..e38ce3053 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -10,6 +10,7 @@ import { getSupportCodeLibrary } from './support' import { Console } from 'console' import { mergeEnvironment } from './environment' import { getFilteredPicklesAndErrors } from './gherkin' +import { initializePlugins } from './plugins' /** * Execute a Cucumber test run. @@ -47,10 +48,13 @@ export async function runCucumber( requireModules: supportCoordinates.requireModules, }) + const plugins = await initializePlugins(configuration, environment) + const eventBroadcaster = new EventEmitter() if (onMessage) { eventBroadcaster.on('envelope', onMessage) } + eventBroadcaster.on('envelope', (value) => plugins.emit('message', value)) const eventDataCollector = new EventDataCollector(eventBroadcaster) let formatterStreamError = false @@ -90,6 +94,7 @@ export async function runCucumber( ) }) await cleanup() + await plugins.cleanup() return { success: false, support: supportCodeLibrary, @@ -117,6 +122,7 @@ export async function runCucumber( }) const success = await runtime.start() await cleanup() + await plugins.cleanup() return { success: success && !formatterStreamError, diff --git a/src/formatter/index.ts b/src/formatter/index.ts index 995fb142a..ba4c381d3 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -6,7 +6,6 @@ import { ISupportCodeLibrary } from '../support_code_library_builder/types' import { WriteStream as FsWriteStream } from 'fs' import { WriteStream as TtyWriteStream } from 'tty' import { EventEmitter } from 'events' -import HttpStream from './http_stream' import { valueOrDefault } from '../value_checker' import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' @@ -23,11 +22,7 @@ export interface FormatOptions { [customKey: string]: any } -export type IFormatterStream = - | FsWriteStream - | TtyWriteStream - | PassThrough - | HttpStream +export type IFormatterStream = FsWriteStream | TtyWriteStream | PassThrough export type IFormatterLogFn = (buffer: string | Uint8Array) => void export type IFormatterCleanupFn = () => Promise diff --git a/src/plugin/index.ts b/src/plugin/index.ts new file mode 100644 index 000000000..dc5af7d7f --- /dev/null +++ b/src/plugin/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './plugin_manager' diff --git a/src/plugin/plugin_manager.ts b/src/plugin/plugin_manager.ts new file mode 100644 index 000000000..f825c8076 --- /dev/null +++ b/src/plugin/plugin_manager.ts @@ -0,0 +1,37 @@ +import { Envelope } from '@cucumber/messages' +import { Plugin, PluginCleanup } from './types' +import { IRunEnvironment, IRunOptions } from '../api' + +export class PluginManager { + private handlers: Array<(value: Envelope) => void> = [] + private cleanupFns: PluginCleanup[] = [] + + constructor(private pluginFns: Plugin[]) {} + + private async register(event: 'message', handler: (value: Envelope) => void) { + this.handlers.push(handler) + } + + async init(configuration: IRunOptions, environment: IRunEnvironment) { + for (const pluginFn of this.pluginFns) { + const cleanupFn = await pluginFn({ + on: this.register.bind(this), + configuration, + environment, + }) + if (cleanupFn) { + this.cleanupFns.push(cleanupFn) + } + } + } + + emit(event: 'message', value: Envelope): void { + this.handlers.forEach((handler) => handler(value)) + } + + async cleanup(): Promise { + for (const cleanupFn of this.cleanupFns) { + await cleanupFn() + } + } +} diff --git a/src/plugin/types.ts b/src/plugin/types.ts new file mode 100644 index 000000000..b1a46b436 --- /dev/null +++ b/src/plugin/types.ts @@ -0,0 +1,12 @@ +import { IRunEnvironment, IRunOptions } from '../api' +import { Envelope } from '@cucumber/messages' + +export type PluginCleanup = () => any | void | Promise + +interface PluginContext { + on: (event: 'message', handler: (value: Envelope) => void) => void + configuration: IRunOptions + environment: IRunEnvironment +} + +export type Plugin = (context: PluginContext) => Promise diff --git a/src/formatter/http_stream.ts b/src/publish/http_stream.ts similarity index 100% rename from src/formatter/http_stream.ts rename to src/publish/http_stream.ts diff --git a/src/publish/index.ts b/src/publish/index.ts new file mode 100644 index 000000000..c22c3eff0 --- /dev/null +++ b/src/publish/index.ts @@ -0,0 +1,3 @@ +import { publishPlugin } from './publish_plugin' + +export default publishPlugin diff --git a/src/publish/publish_plugin.ts b/src/publish/publish_plugin.ts new file mode 100644 index 000000000..c486a19f7 --- /dev/null +++ b/src/publish/publish_plugin.ts @@ -0,0 +1,49 @@ +import { Plugin } from '../plugin' +import { DEFAULT_CUCUMBER_PUBLISH_URL } from '../formatter/publish' +import HttpStream from './http_stream' +import { Writable } from 'stream' +import { IFormatterStream } from '../formatter' +import { supportsColor } from 'supports-color' +import hasAnsi from 'has-ansi' +import stripAnsi from 'strip-ansi' + +export const publishPlugin: Plugin = async ({ + on, + configuration, + environment, +}) => { + if (!configuration.formats.publish) { + return undefined + } + const { url = DEFAULT_CUCUMBER_PUBLISH_URL, token } = + configuration.formats.publish + const headers: { [key: string]: string } = {} + if (token !== undefined) { + headers.Authorization = `Bearer ${token}` + } + const stream = new HttpStream(url, 'GET', headers) + const readerStream = new Writable({ + objectMode: true, + write: function (responseBody: string, encoding, writeCallback) { + environment.stderr.write( + sanitisePublishOutput(responseBody, environment.stderr) + '\n' + ) + writeCallback() + }, + }) + stream.pipe(readerStream) + on('message', (value) => stream.write(JSON.stringify(value) + '\n')) + return () => stream.end() +} +/* +This is because the Cucumber Reports service returns a pre-formatted console message +including ANSI escapes, so if our stderr stream doesn't support those we need to +strip them back out. Ideally we should get structured data from the service and +compose the console message on this end. + */ +function sanitisePublishOutput(raw: string, stderr: IFormatterStream) { + if (!supportsColor(stderr) && hasAnsi(raw)) { + return stripAnsi(raw) + } + return raw +} From 33058aaf6e551e0d40cce8bd0336591df45c713b Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 16:10:23 +0100 Subject: [PATCH 2/7] move stray bits of config out of other areas --- src/formatter/publish.ts | 2 -- src/publish/publish_plugin.ts | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 src/formatter/publish.ts diff --git a/src/formatter/publish.ts b/src/formatter/publish.ts deleted file mode 100644 index ca16d98c3..000000000 --- a/src/formatter/publish.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_CUCUMBER_PUBLISH_URL = - 'https://messages.cucumber.io/api/reports' diff --git a/src/publish/publish_plugin.ts b/src/publish/publish_plugin.ts index c486a19f7..a34758faf 100644 --- a/src/publish/publish_plugin.ts +++ b/src/publish/publish_plugin.ts @@ -1,5 +1,4 @@ import { Plugin } from '../plugin' -import { DEFAULT_CUCUMBER_PUBLISH_URL } from '../formatter/publish' import HttpStream from './http_stream' import { Writable } from 'stream' import { IFormatterStream } from '../formatter' @@ -7,6 +6,8 @@ import { supportsColor } from 'supports-color' import hasAnsi from 'has-ansi' import stripAnsi from 'strip-ansi' +const DEFAULT_CUCUMBER_PUBLISH_URL = 'https://messages.cucumber.io/api/reports' + export const publishPlugin: Plugin = async ({ on, configuration, From 4477a4e8f130f0d32de301bb34743b4067da8d5b Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 16:12:16 +0100 Subject: [PATCH 3/7] rename for clarity --- src/api/run_cucumber.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index e38ce3053..da557d13f 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -58,7 +58,7 @@ export async function runCucumber( const eventDataCollector = new EventDataCollector(eventBroadcaster) let formatterStreamError = false - const cleanup = await initializeFormatters({ + const cleanupFormatters = await initializeFormatters({ env, cwd, stdout, @@ -93,7 +93,7 @@ export async function runCucumber( `Parse error in "${parseError.source.uri}" ${parseError.message}` ) }) - await cleanup() + await cleanupFormatters() await plugins.cleanup() return { success: false, @@ -121,7 +121,7 @@ export async function runCucumber( options: configuration.runtime, }) const success = await runtime.start() - await cleanup() + await cleanupFormatters() await plugins.cleanup() return { From 2a12666498cc6d14c636a93482c81a339bbb3c2f Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 16:51:56 +0100 Subject: [PATCH 4/7] get last scenario working; handle errors --- features/publish.feature | 2 +- src/api/plugins.ts | 3 ++- src/api/run_cucumber.ts | 4 ++-- src/plugin/plugin_manager.ts | 7 ++++++- src/plugin/types.ts | 1 + src/publish/publish_plugin.ts | 2 ++ 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/features/publish.feature b/features/publish.feature index debcf067a..1267dac87 100644 --- a/features/publish.feature +++ b/features/publish.feature @@ -106,7 +106,7 @@ Feature: Publish reports @spawn Scenario: when results are not published due to an error raised by the server, the banner is displayed When I run cucumber-js with env `CUCUMBER_PUBLISH_TOKEN=keyboardcat` - Then it fails + Then it passes And the error output contains the text: """ ┌─────────────────────┐ diff --git a/src/api/plugins.ts b/src/api/plugins.ts index 0e4be9593..e4cdabb7e 100644 --- a/src/api/plugins.ts +++ b/src/api/plugins.ts @@ -7,11 +7,12 @@ const INTERNAL_PLUGINS: Record = { } export async function initializePlugins( + logger: Console, configuration: IRunOptions, environment: IRunEnvironment ): Promise { // eventually we'll load plugin packages here const pluginManager = new PluginManager(Object.values(INTERNAL_PLUGINS)) - await pluginManager.init(configuration, environment) + await pluginManager.init(logger, configuration, environment) return pluginManager } diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index da557d13f..3279cf9a9 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -26,7 +26,7 @@ export async function runCucumber( onMessage?: (message: Envelope) => void ): Promise { const { cwd, stdout, stderr, env } = mergeEnvironment(environment) - const logger = new Console(stdout, stderr) + const logger = new Console(stderr) const newId = IdGenerator.uuid() const supportCoordinates = @@ -48,7 +48,7 @@ export async function runCucumber( requireModules: supportCoordinates.requireModules, }) - const plugins = await initializePlugins(configuration, environment) + const plugins = await initializePlugins(logger, configuration, environment) const eventBroadcaster = new EventEmitter() if (onMessage) { diff --git a/src/plugin/plugin_manager.ts b/src/plugin/plugin_manager.ts index f825c8076..09a05b447 100644 --- a/src/plugin/plugin_manager.ts +++ b/src/plugin/plugin_manager.ts @@ -12,10 +12,15 @@ export class PluginManager { this.handlers.push(handler) } - async init(configuration: IRunOptions, environment: IRunEnvironment) { + async init( + logger: Console, + configuration: IRunOptions, + environment: IRunEnvironment + ) { for (const pluginFn of this.pluginFns) { const cleanupFn = await pluginFn({ on: this.register.bind(this), + logger, configuration, environment, }) diff --git a/src/plugin/types.ts b/src/plugin/types.ts index b1a46b436..3abe7b999 100644 --- a/src/plugin/types.ts +++ b/src/plugin/types.ts @@ -5,6 +5,7 @@ export type PluginCleanup = () => any | void | Promise interface PluginContext { on: (event: 'message', handler: (value: Envelope) => void) => void + logger: Console configuration: IRunOptions environment: IRunEnvironment } diff --git a/src/publish/publish_plugin.ts b/src/publish/publish_plugin.ts index a34758faf..794955f5e 100644 --- a/src/publish/publish_plugin.ts +++ b/src/publish/publish_plugin.ts @@ -10,6 +10,7 @@ const DEFAULT_CUCUMBER_PUBLISH_URL = 'https://messages.cucumber.io/api/reports' export const publishPlugin: Plugin = async ({ on, + logger, configuration, environment, }) => { @@ -33,6 +34,7 @@ export const publishPlugin: Plugin = async ({ }, }) stream.pipe(readerStream) + stream.on('error', (error: Error) => logger.error(error.message)) on('message', (value) => stream.write(JSON.stringify(value) + '\n')) return () => stream.end() } From 186eb16dea9e8d01bed17cfc5b5b34cf135d32aa Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 17:03:49 +0100 Subject: [PATCH 5/7] loosen stream types --- src/api/types.ts | 7 ++++--- src/formatter/index.ts | 10 ++++------ src/publish/publish_plugin.ts | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/api/types.ts b/src/api/types.ts index 046cb70b1..847f5dc7d 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,8 +1,9 @@ import { ISupportCodeLibrary } from '../support_code_library_builder/types' -import { FormatOptions, IFormatterStream } from '../formatter' +import { FormatOptions } from '../formatter' import { PickleOrder } from '../models/pickle_order' import { IRuntimeOptions } from '../runtime' import { IConfiguration } from '../configuration' +import { Writable } from 'stream' /** * @public @@ -158,11 +159,11 @@ export interface IRunEnvironment { /** * Writable stream where the test run's main output is written (defaults to `process.stdout` if omitted). */ - stdout?: IFormatterStream + stdout?: Writable /** * Writable stream where the test run's warning/error output is written (defaults to `process.stderr` if omitted). */ - stderr?: IFormatterStream + stderr?: Writable /** * Environment variables (defaults to `process.env` if omitted). */ diff --git a/src/formatter/index.ts b/src/formatter/index.ts index ba4c381d3..ebd316848 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -1,10 +1,8 @@ import { IColorFns } from './get_color_fns' import { EventDataCollector } from './helpers' import StepDefinitionSnippetBuilder from './step_definition_snippet_builder' -import { PassThrough, Writable as WritableStream } from 'stream' +import { Writable } from 'stream' import { ISupportCodeLibrary } from '../support_code_library_builder/types' -import { WriteStream as FsWriteStream } from 'fs' -import { WriteStream as TtyWriteStream } from 'tty' import { EventEmitter } from 'events' import { valueOrDefault } from '../value_checker' import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' @@ -22,7 +20,7 @@ export interface FormatOptions { [customKey: string]: any } -export type IFormatterStream = FsWriteStream | TtyWriteStream | PassThrough +export type IFormatterStream = Writable export type IFormatterLogFn = (buffer: string | Uint8Array) => void export type IFormatterCleanupFn = () => Promise @@ -34,7 +32,7 @@ export interface IFormatterOptions { log: IFormatterLogFn parsedArgvOptions: FormatOptions snippetBuilder: StepDefinitionSnippetBuilder - stream: WritableStream + stream: Writable cleanup: IFormatterCleanupFn supportCodeLibrary: ISupportCodeLibrary } @@ -45,7 +43,7 @@ export default class Formatter { protected eventDataCollector: EventDataCollector protected log: IFormatterLogFn protected snippetBuilder: StepDefinitionSnippetBuilder - protected stream: WritableStream + protected stream: Writable protected supportCodeLibrary: ISupportCodeLibrary protected printAttachments: boolean private readonly cleanup: IFormatterCleanupFn diff --git a/src/publish/publish_plugin.ts b/src/publish/publish_plugin.ts index 794955f5e..4d785f28c 100644 --- a/src/publish/publish_plugin.ts +++ b/src/publish/publish_plugin.ts @@ -1,7 +1,6 @@ import { Plugin } from '../plugin' import HttpStream from './http_stream' import { Writable } from 'stream' -import { IFormatterStream } from '../formatter' import { supportsColor } from 'supports-color' import hasAnsi from 'has-ansi' import stripAnsi from 'strip-ansi' @@ -44,7 +43,7 @@ including ANSI escapes, so if our stderr stream doesn't support those we need to strip them back out. Ideally we should get structured data from the service and compose the console message on this end. */ -function sanitisePublishOutput(raw: string, stderr: IFormatterStream) { +function sanitisePublishOutput(raw: string, stderr: Writable) { if (!supportsColor(stderr) && hasAnsi(raw)) { return stripAnsi(raw) } From 7b2c39841b815436ce10d90bff33b5d8e9d6c949 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 23 Jul 2022 18:33:38 +0100 Subject: [PATCH 6/7] sort out typing --- src/plugin/plugin_manager.ts | 20 +++++++++++++------- src/plugin/types.ts | 13 ++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/plugin/plugin_manager.ts b/src/plugin/plugin_manager.ts index 09a05b447..4ebfd70a9 100644 --- a/src/plugin/plugin_manager.ts +++ b/src/plugin/plugin_manager.ts @@ -1,15 +1,21 @@ -import { Envelope } from '@cucumber/messages' -import { Plugin, PluginCleanup } from './types' +import { Plugin, PluginCleanup, PluginEvents } from './types' import { IRunEnvironment, IRunOptions } from '../api' +type HandlerRegistry = { + [K in keyof PluginEvents]: Array<(value: PluginEvents[K]) => void> +} + export class PluginManager { - private handlers: Array<(value: Envelope) => void> = [] + private handlers: HandlerRegistry = { message: [] } private cleanupFns: PluginCleanup[] = [] constructor(private pluginFns: Plugin[]) {} - private async register(event: 'message', handler: (value: Envelope) => void) { - this.handlers.push(handler) + private async register( + event: K, + handler: (value: PluginEvents[K]) => void + ) { + this.handlers[event].push(handler) } async init( @@ -30,8 +36,8 @@ export class PluginManager { } } - emit(event: 'message', value: Envelope): void { - this.handlers.forEach((handler) => handler(value)) + emit(event: K, value: PluginEvents[K]): void { + this.handlers[event].forEach((handler) => handler(value)) } async cleanup(): Promise { diff --git a/src/plugin/types.ts b/src/plugin/types.ts index 3abe7b999..b4c8e89da 100644 --- a/src/plugin/types.ts +++ b/src/plugin/types.ts @@ -1,13 +1,20 @@ import { IRunEnvironment, IRunOptions } from '../api' import { Envelope } from '@cucumber/messages' -export type PluginCleanup = () => any | void | Promise +export interface PluginEvents { + message: Envelope +} -interface PluginContext { - on: (event: 'message', handler: (value: Envelope) => void) => void +export interface PluginContext { + on: ( + event: K, + handler: (value: PluginEvents[K]) => void + ) => void logger: Console configuration: IRunOptions environment: IRunEnvironment } +export type PluginCleanup = () => any | void | Promise + export type Plugin = (context: PluginContext) => Promise From dc308f1ea2798ea4766f2a86d79cb9a53ff0c10c Mon Sep 17 00:00:00 2001 From: David Goss Date: Sun, 7 Aug 2022 10:16:27 +0100 Subject: [PATCH 7/7] add draft documentation --- docs/plugins.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/plugins.md diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..d4c0f17ce --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,61 @@ +🚨 This functionality is a work in progress and not yet available in a released version of Cucumber. If you have any +feedback about how plugins are going to work, please jump into the comments +on 🚨 + +- - - + +# Plugins + +You can extend Cucumber's functionality by writing 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 API is described below. Our own Publish functionality is [implemented as a plugin](../src/publish/publish_plugin.ts), which you might find useful as an example. + +## Writing a plugin + +A plugin in its simplest form is a function. It should be the default export of your plugin package. Here's the signature: + +```js +export default async({ + on, + logger, + configuration, + environment +}) => { + // do stuff here +} +``` + +### Lifecycle + +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 - Cucumber will await the promise. + +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 (and awaited if it returns a promise) before Cucumber exits. + +A plugin function accepts a single argument which provides the context for your plugin. It has: + +- `on(event: string, handler: Function)` - function for registering handlers for events (see below for supported events) - you can call this as many times as you'd like +- `logger` - a console instance that directs output to stderr (or other appropriate stream) +- `configuration` - the final resolved configuration object being used for this execution of Cucumber +- `environment` - details of the environment for this execution of Cucumber + +### Events + +These are the events for which you can register handlers: + +| Name | Signature | Notes | +|-----------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `message` | `(message: Envelope) => void` | Cucumber emits a message for all significant events over the course of a test run. These are most commonly consumed by formatters, but have other uses too. Note that you can do async work in this handler, but Cucumber won't await the promise. | + + + + + + + + + +