diff --git a/CHANGELOG.md b/CHANGELOG.md
index d808b3445..76ad28808 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Please see [CONTRIBUTING.md](/~https://github.com/cucumber/cucumber/blob/master/CO
See [./docs/cli.md#printing-attachments-details](/~https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#printing-attachments-details) for more info.
([#1136](/~https://github.com/cucumber/cucumber-js/issues/1136)
[#1721](/~https://github.com/cucumber/cucumber-js/pull/1721))
+- Support for configuration to be objects instead of argv strings, and for configuration files in ESM and JSON formats ([#1952](/~https://github.com/cucumber/cucumber-js/pull/1952))
### Fixed
- Warn users who are on an unsupported node version ([#1922](/~https://github.com/cucumber/cucumber-js/pull/1922))
diff --git a/README.md b/README.md
index ccac4c748..b981ea6ec 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ If you learn best by example, we have [a repo with several example projects](htt
The following documentation is for `main`, which might contain some unreleased features. See below the documentation for older versions.
* [CLI](/docs/cli.md)
+* [Configuration](/docs/configuration.md)
* Support Files
* [World](/docs/support_files/world.md)
* [Step Definitions](/docs/support_files/step_definitions.md)
@@ -56,14 +57,17 @@ The following documentation is for `main`, which might contain some unreleased f
* [Attachments](/docs/support_files/attachments.md)
* [API Reference](/docs/support_files/api_reference.md)
* Guides
- * [Dry Run](./docs/dry_run.md)
+ * [Dry run](./docs/dry_run.md)
* [ES Modules](./docs/esm.md)
+ * [Failing fast](./docs/fail_fast.md)
+ * [Filtering](./docs/filtering.md)
* [Formatters](./docs/formatters.md)
* [Running in parallel](./docs/parallel.md)
* [Profiles](./docs/profiles.md)
* [Rerunning just failures](./docs/rerun.md)
* [Retrying flaky scenarios](./docs/retry.md)
* [Snippets for undefined steps](./docs/snippets.md)
+ * [Transpiling (from TypeScript etc)](./docs/transpiling.md)
* [FAQ](/docs/faq.md)
### Documentation for older versions
diff --git a/compatibility/cck_spec.ts b/compatibility/cck_spec.ts
index 172a685b5..1bed551cd 100644
--- a/compatibility/cck_spec.ts
+++ b/compatibility/cck_spec.ts
@@ -9,8 +9,7 @@ import { ignorableKeys } from '../features/support/formatter_output_helpers'
import * as messages from '@cucumber/messages'
import * as messageStreams from '@cucumber/message-streams'
import util from 'util'
-import { runCucumber } from '../src/api'
-import { IRunConfiguration } from '../src/configuration'
+import { runCucumber, IRunnableConfiguration } from '../src/api'
import { Envelope } from '@cucumber/messages'
const asyncPipeline = util.promisify(pipeline)
@@ -30,9 +29,13 @@ describe('Cucumber Compatibility Kit', () => {
const actualMessages: Envelope[] = []
const stdout = new PassThrough()
const stderr = new PassThrough()
- const runConfiguration: IRunConfiguration = {
+ const runConfiguration: IRunnableConfiguration = {
sources: {
+ defaultDialect: 'en',
paths: [`${CCK_FEATURES_PATH}/${suiteName}/${suiteName}${extension}`],
+ names: [],
+ tagExpression: '',
+ order: 'defined',
},
support: {
requireModules: ['ts-node/register'],
@@ -42,7 +45,20 @@ describe('Cucumber Compatibility Kit', () => {
importPaths: [],
},
runtime: {
+ dryRun: false,
+ failFast: false,
+ filterStacktraces: true,
+ parallel: 0,
retry: suiteName === 'retry' ? 2 : 0,
+ retryTagFilter: '',
+ strict: true,
+ worldParameters: {},
+ },
+ formats: {
+ stdout: 'summary',
+ files: {},
+ options: {},
+ publish: false,
},
}
await runCucumber(
diff --git a/cucumber.js b/cucumber.js
deleted file mode 100644
index caa4fc985..000000000
--- a/cucumber.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- default: [
- '--require-module ts-node/register',
- '--require features/**/*.ts',
- `--format progress-bar`,
- '--format rerun:@rerun.txt',
- '--format usage:reports/usage.txt',
- '--format message:reports/messages.ndjson',
- '--format html:reports/html-formatter.html',
- '--retry 2',
- '--retry-tag-filter @flaky',
- '--publish-quiet',
- ].join(' '),
-}
diff --git a/cucumber.json b/cucumber.json
new file mode 100644
index 000000000..6be20a0fc
--- /dev/null
+++ b/cucumber.json
@@ -0,0 +1,20 @@
+{
+ "default": {
+ "requireModule": [
+ "ts-node/register"
+ ],
+ "require": [
+ "features/**/*.ts"
+ ],
+ "format": [
+ "progress-bar",
+ "rerun:@rerun.txt",
+ "usage:reports/usage.txt",
+ "message:reports/messages.ndjson",
+ "html:reports/html-formatter.html"
+ ],
+ "retry": 2,
+ "retryTagFilter": "@flaky",
+ "publishQuiet": true
+ }
+}
diff --git a/docs/cli.md b/docs/cli.md
index 9223e43fa..e33381cba 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -1,178 +1,59 @@
# CLI
-Cucumber.js includes an executable file to run the features. After installing Cucumber in your project, you can run it with:
+Cucumber includes an executable file to run your scenarios. After installing the `@cucumber/cucumber` package, you can run it directly:
``` shell
$ ./node_modules/.bin/cucumber-js
```
-**Note on global installs:** Cucumber does not work when installed globally because cucumber
-needs to be required in your support files and globally installed modules cannot be required.
-
-## Running specific features
-
-* Specify a [glob](/~https://github.com/isaacs/node-glob) pattern
- * `$ cucumber-js features/**/*.feature`
-* Specify a feature directory
- * `$ cucumber-js features/dir`
-* Specify a feature file
- * `$ cucumber-js features/my_feature.feature`
-* Specify a scenario by its line number
- * `$ cucumber-js features/my_feature.feature:3`
-* Specify a scenario by its name matching a regular expression
- * `$ cucumber-js --name "topic 1"`
- * `$ cucumber-js --name "^start.+end$"`
- * If used multiple times, the scenario name needs to match only one of the names supplied
- * To escape special regex characters in scenario name, use backslash e.g., `\(Scenario Name\)`
-* Use [Tags](#tags)
-
-## Loading support files
-
-By default, the following files are loaded:
-* If the features live in a `features` directory (at any level)
- * `features/**/*.(js|mjs)`
-* Otherwise
- * `
/**/*.(js|mjs)` for each directory containing the selected features
+Or via a [`package.json` script](https://docs.npmjs.com/cli/v8/using-npm/scripts):
-With the defaults described above, `.js` files are loaded via `require()`, whereas `.mjs` files are loaded via `import()`.
-
-Alternatively, you can use either or both of these options to explicitly load support files before executing the features:
+```json
+{
+ "scripts": {
+ "cucumber": "cucumber-js"
+ }
+}
+```
-- `--require ` - loads via `require()` (legacy)
-- `--import ` - loads via `import()`
+Or via [npx](https://docs.npmjs.com/cli/v8/commands/npx):
-Both options use [glob](/~https://github.com/isaacs/node-glob) patterns and may be used multiple times in order to e.g. load files from several different locations.
+``` shell
+$ npx cucumber-js
+```
-_Note that once you specify any `--require` or `--import` options, the defaults described above are no longer applied._
+**Note on global installs:** Cucumber does not work when installed globally because `@cucumber/cucumber`
+needs to be required in your support files and globally installed modules cannot be required.
-## Formats
+## Options
-Use `--format ` to specify the format of the output.
+All the [standard configuration options](./configuration.md#options) can be provided via the CLI.
-See [Formatters](./formatters.md).
+Additionally, there are a few options that are specific to the CLI:
-### Officially-supported standalone formatters
+| Option | Type | Repeatable | Description |
+|--------------------|------------|------------|-----------------------------------------------------------------------------------------|
+| `--config`, `-c` | `string` | No | Path to your configuration file - see [Files](./configuration.md#files) |
+| `--profile`, `-p` | `string[]` | Yes | Profiles from which to include configuration - see [Profiles](./profiles.md) |
+| `--version`, `-v` | `boolean` | No | Print the currently installed version of Cucumber, then exit immediately |
+| `--i18n-keywords` | `string` | No | Print the Gherkin keywords for the given ISO-639-1 language code, then exit immediately |
+| `--i18n-languages` | `boolean` | No | Print the supported languages for Gherkin, then exit immediately |
-* [@cucumber/pretty-formatter](https://www.npmjs.com/package/@cucumber/pretty-formatter) - prints the feature with inline results, colours and custom themes.
+To see the available options for your installed version, run:
-### Format Options
+```shell
+$ cucumber-js --help
+```
-You can pass in format options with `--format-options `.
+## Exiting
-See [Formatters](./formatters.md).
+By default, cucumber exits when the event loop drains. Use the `forceExit` configuration option in order to force shutdown of the event loop when the test run has finished:
-## Exiting
+- In a configuration file `{ forceExit: true }`
+- On the CLI `$ cucumber-js --force-exit`
-By default, cucumber exits when the event loop drains. Use the `--exit` flag in order to force shutdown of the event loop when the test run has finished. This is discouraged, as fixing the issues that causes the hang is a better long term solution. Some potential resources for that are:
+This is discouraged, as fixing the issues that causes the hang is a better long term solution. Some potential resources for that are:
* [Node.js guide to debugging](https://nodejs.org/en/docs/inspector/)
* NPM package [why-is-node-running](https://www.npmjs.com/package/why-is-node-running)
* [Node.js Async Hooks](https://nodejs.org/dist/latest-v8.x/docs/api/async_hooks.html)
* Isolating what scenario or scenarios causes the hang
-
-## --no-strict
-
-disable _strict_ mode.
-
-By default, cucumber works in _strict_ mode, meaning it will fail if there are pending steps.
-
-## Parallel
-
-See [Parallel](./parallel.md).
-
-## Printing Attachments Details
-
-Printing attachments details can be disabled with
-`--fomat-options '{"printAttachments": false}'`.
-
-This option applies to the progress formatter and the summary formatter.
-
-## Profiles
-
-See [Profiles](./profiles.md).
-
-## Tags
-
-Use `--tags ` to run specific features or scenarios. This option is repeatable and the expressions will be merged with an `and` operator.
-`` is a [cucumber tag expression](https://docs.cucumber.io/cucumber/api/#tag-expressions).
-
-## --fail-fast
-
-abort the run on first failure (default: false)
-
-By default, cucumber-js runs the entire suite and reports all the failures. This flag allows a developer workflow where you work on one failure at a time. Combining this feature with rerun files allows you to work through all failures in an efficient manner.
-
-A note on using in conjunction with `--retry`: we consider a test case to have failed if it exhausts retries and still fails, but passed if it passes on a retry having failed previous attempts, so `--fail-fast` does still allow retries to happen.
-
-## Retry failing tests
-
-See [Retry](./retry.md)
-
-## Transpilation
-
-Step definitions and support files can be written in other languages that transpile to JavaScript.
-
-### Simple ES6 support
-
-For instance, for ES6 support with [Babel](https://babeljs.io/) 7 add:
-
-```
---require-module @babel/register
-```
-
-This will effectively call `require('@babel/register')` prior to requiring any support files.
-
-If your files end with an extension other than `js`, make sure to also include the `--require` option to state the required support files. For example, if using [CoffeeScript](https://www.npmjs.com/package/coffeescript):
-
-```
---require-module coffeescript/register --require 'features/**/*.coffee'
-```
-
-### TypeScript
-
-Your `tsconfig.json` should have the `resolveJsonModule` compiler option switched on. Other than that, a pretty standard TypeScript setup should work as expected.
-
-#### With ts-node
-
-If you are using [ts-node](/~https://github.com/TypeStrong/ts-node):
-
-```
---require-module ts-node/register --require 'step-definitions/**/*.ts'
-```
-
-> ⚠️ Some TypeScript setups use `esnext` modules by default,
-> which doesn't marry well with Node. You may consider using commonjs instead.
-> See how to add [extra configuration](#extra-configuration) below.
-
-#### With babel
-
-If you are using babel with [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript):
-
-```
---require-module @babel/register --require 'step-definitions/**/*.ts'
-```
-
-### Extra Configuration
-
-Sometimes the required module (say `@ts-node/register`) needs extra configuration. For example, you might want to configure it such that it prevents the compiled JS being written out to files, and pass some compiler options. In such cases, create a script (say, `tests.setup.js`):
-
-```js
-require('ts-node').register({
- transpileOnly: true,
- compilerOptions: {
- "module": "commonjs",
- "resolveJsonModule": true,
- },
-});
-```
-
-And then require it using the `--require` option:
-
-```
---require tests.setup.js --require 'features/**/*.ts'
-```
-
-Note that the first `--require tests.setup.js` overrides the default require glob, so we'll need to `--require` our support code explicitly too.
-
-## World Parameters
-
-See [World](./support_files/world.md).
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 000000000..1761b5657
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,123 @@
+# Configuration
+
+# Files
+
+You can keep your configuration in a file. Cucumber will look for one of these files in the root of your project, and use the first one it finds:
+
+- `cucumber.js`
+- `cucumber.cjs`
+- `cucumber.mjs`
+- `cucumber.json`
+
+You can also put your file somewhere else and tell Cucumber via the `--config` CLI option:
+
+```shell
+$ cucumber-js --config config/cucumber.js
+```
+
+Here's a concise example of a configuration file in CommonJS format:
+
+```js
+module.exports = {
+ default: {
+ parallel: 2,
+ format: ['html:cucumber-report.html']
+ }
+}
+```
+
+And the same in ESM format:
+
+```js
+export default {
+ parallel: 2,
+ format: ['html:cucumber-report.html']
+}
+```
+
+And the same in JSON format:
+
+```json
+{
+ "default": {
+ "parallel": 2,
+ "format": ["html:cucumber-report.html"]
+ }
+}
+```
+
+Cucumber also supports the configuration being a string of options in the style of the CLI, though this isn't recommended:
+
+```js
+module.exports = {
+ default: '--parallel 2 --format html:cucumber-report.html'
+}
+```
+
+(If you're wondering why the configuration sits within a "default" property, that's to allow for [Profiles](./profiles.md).)
+
+## Options
+
+These options can be used in a configuration file (see [above](#files)) or on the [CLI](./cli.md), or both.
+
+- Where options are repeatable, they are appended/merged if provided more than once.
+- Where options aren't repeatable, the CLI takes precedence over a configuration file.
+
+| Name | Type | Repeatable | CLI Option | Description | Default |
+|-------------------|------------|------------|---------------------------|-------------------------------------------------------------------------------------------------------------------|---------|
+| `paths` | `string[]` | Yes | (as arguments) | Paths to where your feature files are - see [below](#finding-your-features) | [] |
+| `backtrace` | `boolean` | No | `--backtrace`, `-b` | Show the full backtrace for errors | false |
+| `dryRun` | `boolean` | No | `--dry-run`, `-d` | Prepare a test run but don't run it - see [Dry Run](./dry_run.md) | false |
+| `forceExit` | `boolean` | No | `--exit`, `--force-exit` | Explicitly call `process.exit()` after the test run (when run via CLI) - see [CLI](./cli.md) | false |
+| `failFast` | `boolean` | No | `--fail-fast` | Stop running tests when a test fails - see [Fail Fast](./fail_fast.md) | false |
+| `format` | `string[]` | Yes | `--format`, `-f` | Name/path and (optionally) output file path of each formatter to use - see [Formatters](./formatters.md) | [] |
+| `formatOptions` | `object` | Yes | `--format-options` | Options to be provided to formatters - see [Formatters](./formatters.md) | {} |
+| `import` | `string[]` | Yes | `--import`, `-i` | Paths to where your support code is, for ESM - see [ESM](./esm.md) | [] |
+| `language` | `string` | No | `--language` | Default language for your feature files | en |
+| `name` | `string` | No | `--name` | Regular expressions of which scenario names should match one of to be run - see [Filtering](./filtering.md#names) | [] |
+| `order` | `string` | No | `--order` | Run in the order defined, or in a random order | defined |
+| `parallel` | `number` | No | `--parallel` | Run tests in parallel with the given number of worker processes - see [Parallel](./parallel.md) | 0 |
+| `publish` | `boolean` | No | `--publish` | Publish a report of your test run to | false |
+| `publishQuiet` | `boolean` | No | `--publishQuiet` | Don't show info about publishing reports | false |
+| `require` | `string[]` | Yes | `--require`, `-r` | Paths to where your support code is, for CommonJS - see [below](#finding-your-code) | [] |
+| `requireModule` | `string[]` | Yes | `--require-module` | Names of transpilation modules to load, loaded via `require()` - see [Transpiling](./transpiling.md) | [] |
+| `retry` | `number` | No | `--retry` | Retry failing tests up to the given number of times - see [Retry](./retry.md) | 0 |
+| `retryTagFilter` | `string` | Yes | `--retry-tag-filter` | Tag expression to filter which scenarios can be retried - see [Retry](./retry.md) | |
+| `strict` | `boolean` | No | `--strict`, `--no-strict` | Fail the test run if there are pending steps | true |
+| `tags` | `string` | Yes | `--tags`, `-t` | Tag expression to filter which scenarios should be run - see [Filtering](./filtering.md#tags) | |
+| `worldParameters` | `object` | Yes | `--world-parameters` | Parameters to be passed to your World - see [World](./support_files/world.md) | {} |
+
+## Finding your features
+
+By default, Cucumber finds features that match this glob (relative to your project's root directory):
+
+```
+features/**/*.{feature,feature.md}
+```
+
+If your features are somewhere else, you can override this by proving your own [glob](/~https://github.com/isaacs/node-glob) or directory:
+
+- In a configuration file `{ paths: ['somewhere-else/**/*.feature'] }`
+- On the CLI `$ cucumber-js somewhere-else/**/*.feature`
+
+This option is repeatable, so you can provide several values and they'll be combined.
+
+For more granular options to control _which scenarios_ from your features should be run, see [Filtering](./filtering.md).
+
+## Finding your code
+
+By default, Cucumber finds support code files with this logic:
+
+* If the features live in a `features` directory (at any level)
+ * `features/**/*.(js)`
+* Otherwise
+ * `/**/*.(js)` for each directory containing the selected features
+
+If your files are somewhere else, you can override this by proving your own [glob](/~https://github.com/isaacs/node-glob), directory or file path to the `require` configuration option:
+
+- In a configuration file `{ require: ['somewhere-else/support/*.js'] }`
+- On the CLI `$ cucumber-js --require somewhere-else/support/*.js`
+
+Once you specify any `require` options, the defaults described above are no longer applied. The option is repeatable, so you can provide several values and they'll be combined, meaning you can load files from multiple locations.
+
+The default behaviour and the `require` option both use the [legacy CommonJS modules API](https://nodejs.org/api/modules.html) to load your files. If your files are native ES modules, you'll need to use the `import` option instead in the same way, and they'll be loaded with the [new ES modules API](https://nodejs.org/api/esm.html). See [ES Modules](./esm.md) for more on using Cucumber in an ESM project.
diff --git a/docs/dry_run.md b/docs/dry_run.md
index 1f0f8ea0b..369e76412 100644
--- a/docs/dry_run.md
+++ b/docs/dry_run.md
@@ -1,12 +1,11 @@
-# Dry Run
+# Dry run
-You can run cucumber-js in "Dry Run" mode like this:
+You can run cucumber-js in "Dry Run" mode:
-```shell
-$ cucumber-js --dry-run
-```
+- In a configuration file `{ dryRun: true }`
+- On the CLI `$ cucumber-js --dry-run`
-The effect is that cucumber-js will still do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests. Specifically:
+The effect is that Cucumber will still do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests. Specifically:
- No [hooks](./support_files/hooks.md) are executed
- Steps are reported as "skipped" instead of being executed
diff --git a/docs/esm.md b/docs/esm.md
index c8a61787c..1f4747d54 100644
--- a/docs/esm.md
+++ b/docs/esm.md
@@ -2,9 +2,7 @@
You can optionally write your support code (steps, hooks, etc) with native ES modules syntax - i.e. using `import` and `export` statements without transpiling.
-If your support code is written as ESM, you'll need to use the `--import` option to specify your files, rather than the `--require` option.
-
-**Important**: please note that your configuration file referenced for [Profiles](./profiles.md) - aka `cucumber.js` file - must remain a CommonJS file. In a project with `type=module`, you can name the file `cucumber.cjs`, since Node expects `.js` files to be in ESM syntax in such projects.
+If your support code is written as ESM, you'll need to use the `import` configuration option to specify your files, rather than the `require` option, although we do automatically detect and import any `.mjs` files found within your features directory.
Example (adapted from [our original example](./nodejs_example.md)):
@@ -33,4 +31,31 @@ As well as support code, these things can also be in ES modules syntax:
You can use ES modules selectively/incrementally - so you can have a mixture of CommonJS and ESM in the same project.
-When using a transpiler for e.g. TypeScript, ESM isn't supported - you'll need to configure your transpiler to output modules in CommonJS syntax (for now).
+## Configuration file
+
+You can write your [configuration file](./configuration.md#files) in ESM format. Here's an example adapted from our [Profiles](./profiles.md) doc:
+
+```javascript
+const common = {
+ requireModule: ['ts-node/register'],
+ require: ['support/**/*.ts'],
+ worldParameters: {
+ appUrl: process.env.MY_APP_URL || 'http://localhost:3000/'
+ }
+}
+
+export default {
+ ...common,
+ format: ['progress-bar', 'html:cucumber-report.html'],
+}
+
+export const ci = {
+ ...common,
+ format: ['html:cucumber-report.html'],
+ publish: true
+}
+```
+
+## Transpiling
+
+When using a transpiler for e.g. TypeScript, ESM isn't supported - you'll need to configure your transpiler to output modules in CommonJS syntax (for now). See [this GitHub issue](/~https://github.com/cucumber/cucumber-js/issues/1844) for the latest on support for ESM loaders.
diff --git a/docs/fail_fast.md b/docs/fail_fast.md
new file mode 100644
index 000000000..668b2899f
--- /dev/null
+++ b/docs/fail_fast.md
@@ -0,0 +1,8 @@
+# Failing Fast
+
+- In a configuration file `{ failFast: true }`
+- On the CLI `$ cucumber-js --fail-fast`
+
+By default, Cucumber runs the entire suite and reports all the failures. `failFast` allows a developer workflow where you work on one failure at a time. Combining this feature with rerun files allows you to work through all failures in an efficient manner.
+
+A note on using in conjunction with [Retry](./retry.md): we consider a test case to have failed if it exhausts retries and still fails, but passed if it passes on a retry having failed previous attempts, so `failFast` does still allow retries to happen.
diff --git a/docs/filtering.md b/docs/filtering.md
new file mode 100644
index 000000000..84842510a
--- /dev/null
+++ b/docs/filtering.md
@@ -0,0 +1,37 @@
+# Filtering
+
+You can use a few different configurations to have Cucumber filter which scenarios are run. This can be useful when you want to focus on a small subset of scenarios when developing or debugging.
+
+## Paths
+
+You can specify an individual feature file to be run:
+
+- In a configuration file `{ paths: ['features/my_feature.feature'] }`
+- On the CLI `$ cucumber-js features/my_feature.feature`
+
+You can also specify a line within a file to target an individual scenario:
+
+- In a configuration file `{ paths: ['features/my_feature.feature:3'] }`
+- On the CLI `$ cucumber-js features/my_feature.feature:3`
+
+This option is repeatable, so you can provide several values and they'll be combined.
+
+## Names
+
+You can specify a regular expression against which scenario names are tested to filter which should run:
+
+- In a configuration file `{ name: ['^start.+end$'] }`
+- On the CLI `$ cucumber-js --name "^start.+end$"`
+
+To escape special regex characters in scenario name, use backslashes e.g., `\(Scenario Name\)`
+
+This option is repeatable, so you can provide several expressions and they'll all be used, meaning a scenario just needs to match one of them.
+
+## Tags
+
+You can specify a [Cucumber tag expression](https://docs.cucumber.io/cucumber/api/#tag-expressions) to only run scenarios that match it:
+
+- In a configuration file `{ tags: ['@foo or @bar'] }`
+- On the CLI `$ cucumber-js --tags "@foo or @bar"`
+
+This option is repeatable, so you can provide several expressions and they'll be combined with an `and` operator, meaning a scenario needs to match all of them.
diff --git a/docs/formatters.md b/docs/formatters.md
index 08bf6ffee..eb19cd808 100644
--- a/docs/formatters.md
+++ b/docs/formatters.md
@@ -4,7 +4,12 @@ In cucumber-js, Formatters ingest data about your test run in real time and then
cucumber-js provides many built-in Formatters, plus building blocks with which you can [write your own](./custom_formatters.md).
-You can specify one or more formats via the `--format ` CLI option, where `TYPE` is one of:
+You can specify one or more formats via the `format` configuration option:
+
+- In a configuration file `{ format: [''] }`
+- On the CLI `$ cucumber-js --format `
+
+For each value you provide, `TYPE` should be one of:
* The name of one of the built-in formatters (below) e.g. `progress`
* A module/package name e.g. `@cucumber/pretty-formatter`
@@ -12,6 +17,11 @@ You can specify one or more formats via the `--format ` CLI option,
If `PATH` is supplied, the formatter prints to the given file, otherwise it prints to `stdout`.
+For example, this configuration would give you a progress bar as you run, plus JSON and HTML report files:
+
+- In a configuration file `{ format: ['progress-bar', 'json:cucumber-report.json', 'html:cucumber-report.html'] }`
+- On the CLI `$ cucumber-js --format progress-bar --format json:cucumber-report.json --format html:cucumber-report.html`
+
Some notes on specifying Formatters:
* If multiple formatters are specified with the same output, only the last is used.
@@ -19,11 +29,10 @@ Some notes on specifying Formatters:
## Options
-Many formatters, including the built-in ones, support some configurability via options. You can provide this data as a JSON literal via the `--format-options` CLI option, like this:
+Many formatters, including the built-in ones, support some configurability via options. You can provide this data as an object literal via the `formatOptions` configuration option, like this:
-```shell
-$ cucumber-js --format-options '{"someOption":true}'
-```
+- In a configuration file `{ formatOptions: { someOption: true } }`
+- On the CLI `$ cucumber-js --format-options '{"someOption":true}'`
This option is repeatable, so you can use it multiple times and the objects will be merged with the later ones taking precedence.
diff --git a/docs/parallel.md b/docs/parallel.md
index a730f1c5e..d06d35cc4 100644
--- a/docs/parallel.md
+++ b/docs/parallel.md
@@ -1,10 +1,9 @@
# Parallel
-Cucumber supports running scenarios in parallel. The main process becomes a "coordinator" and spins up several separate Node processes to be the "workers". You can enable this with the `--parallel ` CLI option:
+Cucumber supports running scenarios in parallel. The main process becomes a "coordinator" and spins up several separate Node.js processes to be the "workers". You can enable this with the `parallel` configuration option:
-```shell
-$ cucumber-js --parallel 3
-```
+- In a configuration file `{ parallel: 3 }`
+- On the CLI `$ cucumber-js --parallel 3`
The number you provide is the number of workers that will run scenarios in parallel.
diff --git a/docs/profiles.md b/docs/profiles.md
index eaff7cc21..219f8ac8a 100644
--- a/docs/profiles.md
+++ b/docs/profiles.md
@@ -1,8 +1,8 @@
# Profiles
-If you have several permutations of running Cucumber with different CLI options in your project, it might be a bit cumbersome to manage. *Profiles* enable you to declare bundles of configuration and reference them with a single CLI option. You can use multiple profiles at once and you can still supply options directly on the command line when using profiles.
+If you have several permutations of running Cucumber with different options in your project, it might be a bit cumbersome to manage. *Profiles* enable you to declare bundles of configuration and reference them with a single CLI option. You can use multiple profiles at once and you can still supply options directly on the CLI when using profiles.
-Profiles are invoked from the command line using the `--profile` option.
+Profiles are invoked from the command line using the `--profile` CLI option.
```shell
$ cucumber-js --profile my_profile
@@ -16,31 +16,48 @@ $ cucumber-js -p my_profile
## Simple Example
-Let's take the common case of having some things a bit different locally than on a continuous integration server. Here's the command we've been running locally:
+Let's take the common case of having some things a bit different locally than on a continuous integration server. Here's the configuration we've been running locally:
-```shell
-$ cucumber-js --require-module ts-node/register --require 'support/**/*./ts' --world-parameters '{\"appUrl\":\"http://localhost:3000/\"}' --format progress-bar --format html:./cucumber-report.html
+```javascript
+module.exports = {
+ default: {
+ format: ['progress-bar', 'html:cucumber-report.html'],
+ requireModule: ['ts-node/register'],
+ require: ['support/**/*.ts'],
+ worldParameters: {
+ appUrl: 'http://localhost:3000/'
+ }
+ }
+}
```
For argument's sake, we'll want these changes in CI:
- The URL for the app (maybe dynamically provisioned?)
- The formatters we want to use
+- Publish a report
-To start using Profiles, we just need to create a `cucumber.js` file in our project's root. This file must be a CommonJS module, so when using Cucumber with ES6 modules the file will need to be named `cucumber.cjs`.
-
-This file is a simple JavaScript module that exports an object with profile names as keys and CLI options as values. We can lean on JavaScript to reduce duplication and grab things dynamically as needed. Here's what we might write to address the needs described above:
+Adding profiles is a matter of exporting more named slices of configuration from our file. We can lean on JavaScript to reduce duplication and grab things dynamically as needed. Here's what we might write to address the needs described above:
```javascript
-const worldParameters = {
- appUrl: process.env.MY_APP_URL || "http://localhost:3000/"
+const common = {
+ requireModule: ['ts-node/register'],
+ require: ['support/**/*.ts'],
+ worldParameters: {
+ appUrl: process.env.MY_APP_URL || 'http://localhost:3000/'
+ }
}
-const common = `--require-module ts-node/register --require 'support/**/*./ts' --world-parameters '${JSON.stringify(worldParameters)}'`
-
module.exports = {
- 'default': `${common} --format progress-bar --format html:./cucumber-report.html`,
- 'ci': `${common} --format html:./cucumber-report.html --publish`
+ default: {
+ ...common,
+ format: ['progress-bar', 'html:cucumber-report.html'],
+ },
+ ci: {
+ ...common,
+ format: ['html:cucumber-report.html'],
+ publish: true
+ }
}
```
@@ -62,7 +79,7 @@ The above will result in `error: unknown option '--myArgument'`.
At first glance this can create problems for test suites that need configuration on the fly, particularly web page test suites that need to run the exact same tests against different browser engines and viewport sizes. However, profiles can be used to work around this problem.
-The intended method for passing arguments to tests is the `--world-parameters` CLI option, discussed in detail in the section on the [World](./support_files/world.md) object. As this argument expects a JSON literal it can be very tedious to use directly. However, we can use a profile as an alias for each of these options.
+The intended method for passing arguments to tests is the `worldParameters` configuration option, discussed in detail in the section on the [World](./support_files/world.md) object. As this argument expects a JSON literal it can be very tedious to use directly. However, we can use a profile as an alias for each of these options.
Consider the following profile configuration file:
@@ -84,16 +101,6 @@ $ cucumber-js -p webkit -p phone
The world parameter arguments from the two profile calls will be merged. If you pass profiles that try to set the same parameter, the last one passed in the chain will win out.
-## Using another file than `cucumber.js`
-
-Run `cucumber-js` with `--config` - or `-c` - to specify your configuration file if it is something else than the default `cucumber.js`.
-
-```shell
-$ cucumber-js --config .cucumber-rc.js
-```
-
-**NOTE:** The extension remains controlled by whether your project is configured to use ES6 modules. If it is, you have to use the `cjs` extension.
-
## Summary
- Profiles are used to group common cli arguments together for easy reuse.
- They can also be used to create world-parameter options rather than trying to use a JSON literal on the command line.
diff --git a/docs/rerun.md b/docs/rerun.md
index 6cf07292b..e64152b4a 100644
--- a/docs/rerun.md
+++ b/docs/rerun.md
@@ -13,9 +13,8 @@ Rerun makes this kind of workflow convenient, so you don't have to hand-craft co
First, enable the `rerun` formatter every time you run cucumber-js:
-```shell
---format rerun:@rerun.txt
-```
+- In a configuration file `{ format: ['rerun:@rerun.txt'] }`
+- On the CLI `$ cucumber-js --format rerun:@rerun.txt`
You can do this via the CLI, or more likely via a [default profile](./profiles.md).
@@ -51,8 +50,7 @@ In other words, the one we fixed has passed and thus dropped off. We can repeat
By default, entries in the rerun file are separated by newlines. This can be overwritten via a [format option](./formatters.md#options):
-```
---format-options '{"rerun": {"separator": ""}}'
-```
+- In a configuration file `{ formatOptions: { rerun: { separator: '' } } }`
+- On the CLI `$ cucumber-js --format-options '{"rerun": {"separator": ""}}'`
This is useful when one needs to rerun failed tests locally by copying a line from a CI log while using a space character as a separator. Note that the rerun file parser can only work with the default separator for now.
diff --git a/docs/retry.md b/docs/retry.md
index 2cbdd953c..456256fa0 100644
--- a/docs/retry.md
+++ b/docs/retry.md
@@ -2,11 +2,10 @@
*Note: if you want a mechanism to rerun just the failed scenarios when doing TDD, take a look at [Rerun](./rerun.md) instead.*
-If you have a flaky scenario (e.g. failing 10% of the time for some reason), you can use *Retry* to have Cucumber attempt it multiple times until either it passes or the maximum number of attempts is reached. You enable this via the `--retry ` CLI option, like this:
+If you have a flaky scenario (e.g. failing 10% of the time for some reason), you can use *Retry* to have Cucumber attempt it multiple times until either it passes or the maximum number of attempts is reached. You enable this via the `retry` configuration option, like this:
-```shell
-$ cucumber-js --retry 1
-```
+- In a configuration file `{ retry: 1 }`
+- On the CLI `$ cucumber-js --retry 1`
The number you provide is the number of retries that will be allowed after an initial failure.
@@ -18,11 +17,9 @@ Some notes on how Retry works:
- When a scenario is retried, it runs all hooks and steps again from the start with a fresh [World](./support_files/world.md) - nothing is retained from the failed attempt.
- When a scenario passes on a retry, it's treated as a pass overall in the results, although the details of each attempt are emitted so formatters can access them.
-
## Targeting scenarios
-Using the `--retry` option alone would mean every scenario would be allowed multiple attempts - this almost certainly isn't what you want, assuming you have a small set of flaky scenarios. To target just the relevant scenarios, you can provide a [tag expression](https://cucumber.io/docs/cucumber/api/#tag-expressions) via the `--retry-tag-filter ` CLI option, like this:
+Using the `retry` option alone would mean every scenario would be allowed multiple attempts - this almost certainly isn't what you want, assuming you have a small set of flaky scenarios. To target just the relevant scenarios, you can provide a [tag expression](https://cucumber.io/docs/cucumber/api/#tag-expressions) via the `retryTagFilter` configuration option, like this:
-```shell
-$ cucumber-js --retry 1 --retry-tag-filter @flaky
-```
+- In a configuration file `{ retry: 1, retryTagFilter: '@flaky' }`
+- On the CLI `$ cucumber-js --retry 1 --retry-tag-filter @flaky`
diff --git a/docs/support_files/world.md b/docs/support_files/world.md
index 53fbc93aa..b536f961d 100644
--- a/docs/support_files/world.md
+++ b/docs/support_files/world.md
@@ -55,17 +55,13 @@ Your custom world will also receive these arguments, but it's up to you to decid
Tests often require configuration and environment information. One of the most frequent cases is web page tests that are using a browser driver; things like viewport, browser to use, application URL and so on.
-The world parameters argument allows you to provide this information from the command line. It takes the data in the form of a JSON literal after the `--world-parameters` CLI option, like this:
+The `worldParameters` configuration option allows you to provide this information to Cucumber. It takes the data in the form of an object literal, like this:
-```shell
-$ cucumber-js --world-parameters '{"appUrl":"http://localhost:3000/"}'
-```
+- In a configuration file `{ worldParameters: { appUrl: 'http://localhost:3000/' } }`
+- On the CLI `$ cucumber-js --world-parameters '{"appUrl":"http://localhost:3000/"}'`
This option is repeatable, so you can use it multiple times and the objects will be merged with the later ones taking precedence.
-Using JSON literals on the command line is tedious, but you can use [profiles](../profiles.md) to create aliases for them.
-
-
## Custom Worlds
You might also want to have methods on your World that hooks and steps can access to keep their own code simple. To do this, you can provide your own World class with its own properties and methods that help with your instrumentation, and then call `setWorldConstructor` to tell Cucumber about it.
@@ -151,4 +147,4 @@ This pattern allows for cleaner feature files. Remember that, ideally, scenarios
- It allows you to track test state while maintaining the isolation of each scenario.
- Every scenario gets its own instance of the world. Even on [retry](../retry.md).
- You can use the default world or build your own.
-- You can pass arguments to the world using the `--world-parameters` CLI option.
+- You can pass parameters to the world using the `worldParamerers` configuration option.
diff --git a/docs/transpiling.md b/docs/transpiling.md
new file mode 100644
index 000000000..b67ddf93e
--- /dev/null
+++ b/docs/transpiling.md
@@ -0,0 +1,57 @@
+# Transpiling
+
+Step definitions and support files can be written in syntax/language that compiles to JavaScript, and just-in-time compiled when you run Cucumber.
+
+For example, you might want to use [Babel](https://babeljs.io/):
+
+- In a configuration file `{ requireModule: ['@babel/register'] }`
+- On the CLI `$ cucumber-js --require-module @babel/register`
+
+This would mean any support code loaded with the `require` option would be transpiled first.
+
+## TypeScript
+
+Your `tsconfig.json` should have the `resolveJsonModule` compiler option switched on. Other than that, a pretty standard TypeScript setup should work as expected.
+
+> ⚠️ Some TypeScript setups use `esnext` modules by default,
+> which doesn't marry well with Node. You may consider using commonjs instead.
+> See how to add [extra configuration](#extra-configuration) below.
+
+You'll also need to specify where your support code is, since `.ts` files won't be picked up by default.
+
+### With ts-node
+
+If you are using [ts-node](/~https://github.com/TypeStrong/ts-node):
+
+- In a configuration file `{ requireModule: ['ts-node/register'], require: ['step-definitions/**/*.ts'] }`
+- On the CLI `$ cucumber-js --require-module ts-node/register --require 'step-definitions/**/*.ts'`
+
+### With Babel
+
+If you are using babel with [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript):
+
+- In a configuration file `{ requireModule: ['@babel/register'], require: ['step-definitions/**/*.ts'] }`
+- On the CLI `$ cucumber-js --require-module @babel/register --require 'step-definitions/**/*.ts'`
+
+## Extra Configuration
+
+Sometimes the required module (say `ts-node/register`) needs extra configuration. For example, you might want to configure it such that it prevents the compiled JS being written out to files, and pass some compiler options. In such cases, create a script (say, `tests.setup.js`):
+
+```js
+require('ts-node').register({
+ transpileOnly: true,
+ compilerOptions: {
+ "module": "commonjs",
+ "resolveJsonModule": true,
+ },
+});
+```
+
+And then require it using the `require` option:
+
+- In a configuration file `{ require: ['tests.setup.js', 'features/**/*.ts'] }`
+- On the CLI `$ cucumber-js --require tests.setup.js --require 'features/**/*.ts'`
+
+## ESM
+
+Cucumber doesn't yet support native ESM loader hooks ([see GitHub issue](/~https://github.com/cucumber/cucumber-js/issues/1844)).
diff --git a/features/exit.feature b/features/exit.feature
index a9b73663e..1161c592b 100644
--- a/features/exit.feature
+++ b/features/exit.feature
@@ -31,9 +31,13 @@ Feature: Exit
external process done
"""
- Scenario: exit immediately without waiting for the even loop to drain
- When I run cucumber-js with `--exit`
+ Scenario Outline: exit immediately without waiting for the even loop to drain
+ When I run cucumber-js with ``
Then the output does not contain the text:
"""
external process done
"""
+ Examples:
+ | FLAG |
+ | --exit |
+ | --force-exit |
diff --git a/features/profiles.feature b/features/profiles.feature
index fca00d0fc..ba0771843 100644
--- a/features/profiles.feature
+++ b/features/profiles.feature
@@ -1,7 +1,8 @@
Feature: default command line arguments
In order to prevent users from having to enter the options they use every time
- Users can define cucumber.js with profiles which are groups of command line arguments.
+ Users can define cucumber.js with profiles which are groups of command line arguments
+ or partial configuration objects.
Background:
Given a file named "features/a.feature" with:
@@ -20,7 +21,9 @@ Feature: default command line arguments
"""
module.exports = {
'default': '--format summary',
- dry: '--dry-run',
+ dry: {
+ dryRun: true
+ },
progress: '--format progress'
};
"""
@@ -95,3 +98,22 @@ Feature: default command line arguments
Scenario: specifying a configuration file that doesn't exist
When I run cucumber-js with `--config doesntexist.js`
Then it fails
+
+ Scenario: using a JSON file
+ Given a file named ".cucumber-rc.json" with:
+ """
+ {
+ "default": {
+ "dryRun": true
+ }
+ }
+ """
+ When I run cucumber-js with `--config .cucumber-rc.json`
+ Then it outputs the text:
+ """
+ -
+
+ 1 scenario (1 skipped)
+ 1 step (1 skipped)
+
+ """
diff --git a/features/retry.feature b/features/retry.feature
index 6e114155c..f406fc5d1 100644
--- a/features/retry.feature
+++ b/features/retry.feature
@@ -8,7 +8,7 @@ Feature: Retry flaky tests
When I run cucumber-js with `--retry-tag-filter @flaky`
Then the error output contains the text:
"""
- Error: a positive --retry count must be specified when setting --retry-tag-filter
+ Error: a positive `retry` count must be specified when setting `retryTagFilter`
"""
And it fails
diff --git a/package-lock.json b/package-lock.json
index 485f48aeb..173411630 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,8 @@
"indent-string": "^4.0.0",
"is-stream": "^2.0.0",
"knuth-shuffle-seeded": "^1.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.mergewith": "^4.6.2",
"mz": "^2.7.0",
"progress": "^2.0.3",
"resolve": "^1.19.0",
@@ -38,7 +40,8 @@
"string-argv": "^0.3.1",
"tmp": "^0.2.1",
"util-arity": "^1.1.0",
- "verror": "^1.10.0"
+ "verror": "^1.10.0",
+ "yup": "^0.32.11"
},
"bin": {
"cucumber-js": "bin/cucumber-js"
@@ -53,6 +56,8 @@
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/glob": "7.2.0",
+ "@types/lodash.merge": "^4.6.6",
+ "@types/lodash.mergewith": "^4.6.6",
"@types/mocha": "9.1.0",
"@types/mustache": "4.1.2",
"@types/mz": "2.7.4",
@@ -435,6 +440,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.17.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz",
+ "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
@@ -1041,6 +1057,29 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "node_modules/@types/lodash": {
+ "version": "4.14.179",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz",
+ "integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w=="
+ },
+ "node_modules/@types/lodash.merge": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz",
+ "integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/lodash.mergewith": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz",
+ "integrity": "sha512-RY/8IaVENjG19rxTZu9Nukqh0W2UrYgmBj5sdns4hWRZaV8PqR7wIKHFKzvOTjo4zVRV7sVI+yFhAJql12Kfqg==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -4452,8 +4491,12 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.flattendeep": {
"version": "4.4.0",
@@ -4470,8 +4513,12 @@
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"node_modules/log-symbols": {
"version": "4.1.0",
@@ -4902,6 +4949,11 @@
"thenify-all": "^1.0.0"
}
},
+ "node_modules/nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"node_modules/nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
@@ -5660,6 +5712,11 @@
"integrity": "sha1-zQTv9G9clcOn0EVZHXm14+AfEtc=",
"dev": true
},
+ "node_modules/property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -5945,6 +6002,11 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
+ },
"node_modules/regexp-match-indices": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz",
@@ -6676,6 +6738,11 @@
"node": ">=0.6"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
+ },
"node_modules/trim-newlines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -7235,6 +7302,23 @@
"funding": {
"url": "/~https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yup": {
+ "version": "0.32.11",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
+ "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/lodash": "^4.14.175",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
}
},
"dependencies": {
@@ -7494,6 +7578,14 @@
"integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==",
"dev": true
},
+ "@babel/runtime": {
+ "version": "7.17.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz",
+ "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
"@babel/template": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
@@ -8019,6 +8111,29 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/lodash": {
+ "version": "4.14.179",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz",
+ "integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w=="
+ },
+ "@types/lodash.merge": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz",
+ "integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
+ "@types/lodash.mergewith": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz",
+ "integrity": "sha512-RY/8IaVENjG19rxTZu9Nukqh0W2UrYgmBj5sdns4hWRZaV8PqR7wIKHFKzvOTjo4zVRV7sVI+yFhAJql12Kfqg==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -10600,8 +10715,12 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.flattendeep": {
"version": "4.4.0",
@@ -10618,8 +10737,12 @@
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"log-symbols": {
"version": "4.1.0",
@@ -10934,6 +11057,11 @@
"thenify-all": "^1.0.0"
}
},
+ "nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
@@ -11509,6 +11637,11 @@
"integrity": "sha1-zQTv9G9clcOn0EVZHXm14+AfEtc=",
"dev": true
},
+ "property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -11715,6 +11848,11 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
},
+ "regenerator-runtime": {
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
+ },
"regexp-match-indices": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz",
@@ -12267,6 +12405,11 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
+ "toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
+ },
"trim-newlines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -12682,6 +12825,20 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "yup": {
+ "version": "0.32.11",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
+ "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
+ "requires": {
+ "@babel/runtime": "^7.15.4",
+ "@types/lodash": "^4.14.175",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ }
}
}
}
diff --git a/package.json b/package.json
index 0835fa860..36edb8fea 100644
--- a/package.json
+++ b/package.json
@@ -205,6 +205,8 @@
"indent-string": "^4.0.0",
"is-stream": "^2.0.0",
"knuth-shuffle-seeded": "^1.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.mergewith": "^4.6.2",
"mz": "^2.7.0",
"progress": "^2.0.3",
"resolve": "^1.19.0",
@@ -214,7 +216,8 @@
"string-argv": "^0.3.1",
"tmp": "^0.2.1",
"util-arity": "^1.1.0",
- "verror": "^1.10.0"
+ "verror": "^1.10.0",
+ "yup": "^0.32.11"
},
"devDependencies": {
"@cucumber/compatibility-kit": "9.1.2",
@@ -226,6 +229,8 @@
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/glob": "7.2.0",
+ "@types/lodash.merge": "^4.6.6",
+ "@types/lodash.mergewith": "^4.6.6",
"@types/mocha": "9.1.0",
"@types/mustache": "4.1.2",
"@types/mz": "2.7.4",
diff --git a/src/api/convert_configuration.ts b/src/api/convert_configuration.ts
new file mode 100644
index 000000000..595f68669
--- /dev/null
+++ b/src/api/convert_configuration.ts
@@ -0,0 +1,78 @@
+import {
+ IConfiguration,
+ isTruthyString,
+ OptionSplitter,
+} from '../configuration'
+import { IRunnableConfiguration } from './types'
+
+export async function convertConfiguration(
+ flatConfiguration: IConfiguration,
+ env: NodeJS.ProcessEnv
+): Promise {
+ return {
+ sources: {
+ paths: flatConfiguration.paths,
+ defaultDialect: flatConfiguration.language,
+ names: flatConfiguration.name,
+ tagExpression: flatConfiguration.tags,
+ order: flatConfiguration.order,
+ },
+ support: {
+ requireModules: flatConfiguration.requireModule,
+ requirePaths: flatConfiguration.require,
+ importPaths: flatConfiguration.import,
+ },
+ runtime: {
+ dryRun: flatConfiguration.dryRun,
+ failFast: flatConfiguration.failFast,
+ filterStacktraces: !flatConfiguration.backtrace,
+ parallel: flatConfiguration.parallel,
+ retry: flatConfiguration.retry,
+ retryTagFilter: flatConfiguration.retryTagFilter,
+ strict: flatConfiguration.strict,
+ worldParameters: flatConfiguration.worldParameters,
+ },
+ formats: {
+ stdout:
+ [...flatConfiguration.format]
+ .reverse()
+ .find((option) => !option.includes(':')) ?? 'progress',
+ files: flatConfiguration.format
+ .filter((option) => option.includes(':'))
+ .reduce((mapped, item) => {
+ const [type, target] = OptionSplitter.split(item)
+ return {
+ ...mapped,
+ [target]: type,
+ }
+ }, {}),
+ publish: makePublishConfig(flatConfiguration, env),
+ options: flatConfiguration.formatOptions,
+ },
+ }
+}
+
+function isPublishing(
+ flatConfiguration: IConfiguration,
+ env: NodeJS.ProcessEnv
+): boolean {
+ return (
+ flatConfiguration.publish ||
+ isTruthyString(env.CUCUMBER_PUBLISH_ENABLED) ||
+ env.CUCUMBER_PUBLISH_TOKEN !== undefined
+ )
+}
+
+function makePublishConfig(
+ flatConfiguration: IConfiguration,
+ env: NodeJS.ProcessEnv
+): any {
+ const enabled = isPublishing(flatConfiguration, env)
+ if (!enabled) {
+ return false
+ }
+ return {
+ url: env.CUCUMBER_PUBLISH_URL,
+ token: env.CUCUMBER_PUBLISH_TOKEN,
+ }
+}
diff --git a/src/cli/configuration_builder_spec.ts b/src/api/convert_configuration_spec.ts
similarity index 58%
rename from src/cli/configuration_builder_spec.ts
rename to src/api/convert_configuration_spec.ts
index fa5d19c50..e0c870877 100644
--- a/src/cli/configuration_builder_spec.ts
+++ b/src/api/convert_configuration_spec.ts
@@ -1,19 +1,18 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
-import { buildConfiguration } from './configuration_builder'
-import ArgvParser from './argv_parser'
-const baseArgv = ['/path/to/node', '/path/to/cucumber-js']
+import { convertConfiguration } from './convert_configuration'
+import { DEFAULT_CONFIGURATION } from '../configuration'
-describe('buildConfiguration', () => {
- it('should derive correct defaults', async () => {
- const result = await buildConfiguration(ArgvParser.parse([...baseArgv]), {})
+describe('convertConfiguration', () => {
+ it('should convert defaults correctly', async () => {
+ const result = await convertConfiguration(DEFAULT_CONFIGURATION, {})
expect(result).to.eql({
formats: {
files: {},
options: {},
publish: false,
- stdout: undefined,
+ stdout: 'progress',
},
runtime: {
dryRun: false,
@@ -40,17 +39,17 @@ describe('buildConfiguration', () => {
})
})
- it('should map formatters', async () => {
- const result = await buildConfiguration(
- ArgvParser.parse([
- ...baseArgv,
- '--format',
- 'message',
- '--format',
- 'json:./report.json',
- '--format',
- 'html:./report.html',
- ]),
+ it('should map multiple formatters', async () => {
+ const result = await convertConfiguration(
+ {
+ ...DEFAULT_CONFIGURATION,
+ format: [
+ 'summary',
+ 'message',
+ 'json:./report.json',
+ 'html:./report.html',
+ ],
+ },
{}
)
diff --git a/src/api/formatters.ts b/src/api/formatters.ts
index 386cac427..734f8a842 100644
--- a/src/api/formatters.ts
+++ b/src/api/formatters.ts
@@ -11,7 +11,7 @@ import path from 'path'
import { DEFAULT_CUCUMBER_PUBLISH_URL } from '../formatter/publish'
import HttpStream from '../formatter/http_stream'
import { Writable } from 'stream'
-import { IFormatterConfiguration } from '../configuration'
+import { IRunOptionsFormats } from './types'
export async function initializeFormatters({
cwd,
@@ -20,7 +20,7 @@ export async function initializeFormatters({
onStreamError,
eventBroadcaster,
eventDataCollector,
- configuration = {},
+ configuration,
supportCodeLibrary,
}: {
cwd: string
@@ -29,7 +29,7 @@ export async function initializeFormatters({
onStreamError: () => void
eventBroadcaster: EventEmitter
eventDataCollector: EventDataCollector
- configuration: IFormatterConfiguration
+ configuration: IRunOptionsFormats
supportCodeLibrary: ISupportCodeLibrary
}): Promise<() => Promise> {
async function initializeFormatter(
@@ -46,7 +46,7 @@ export async function initializeFormatters({
eventBroadcaster,
eventDataCollector,
log: stream.write.bind(stream),
- parsedArgvOptions: configuration.options ?? {},
+ parsedArgvOptions: configuration.options,
stream,
cleanup:
stream === stdout
@@ -54,7 +54,7 @@ export async function initializeFormatters({
: promisify(stream.end.bind(stream)),
supportCodeLibrary,
}
- if (doesNotHaveValue(configuration.options?.colorsEnabled)) {
+ if (doesNotHaveValue(configuration.options.colorsEnabled)) {
typeOptions.parsedArgvOptions.colorsEnabled = (
stream as TtyWriteStream
).isTTY
@@ -71,20 +71,14 @@ export async function initializeFormatters({
const formatters: Formatter[] = []
formatters.push(
- await initializeFormatter(
- stdout,
- 'stdout',
- configuration.stdout ?? 'progress'
- )
+ await initializeFormatter(stdout, 'stdout', configuration.stdout)
)
- if (configuration.files) {
- for (const [target, type] of Object.entries(configuration.files)) {
- const stream: IFormatterStream = fs.createWriteStream(null, {
- fd: await fs.open(path.resolve(cwd, target), 'w'),
- })
- formatters.push(await initializeFormatter(stream, target, type))
- }
+ for (const [target, type] of Object.entries(configuration.files)) {
+ const stream: IFormatterStream = fs.createWriteStream(null, {
+ fd: await fs.open(path.resolve(cwd, target), 'w'),
+ })
+ formatters.push(await initializeFormatter(stream, target, type))
}
if (configuration.publish) {
diff --git a/src/api/index.ts b/src/api/index.ts
index 85bc4109b..a4943a48e 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,3 +1,8 @@
-export * from './loadSupport'
-export * from './runCucumber'
+/*
+Anything exported here will be publicly available via `@cucumber/cucumber/api`
+ */
+
+export * from './load_configuration'
+export * from './load_support'
+export * from './run_cucumber'
export * from './types'
diff --git a/src/api/load_configuration.ts b/src/api/load_configuration.ts
new file mode 100644
index 000000000..bd86d50cf
--- /dev/null
+++ b/src/api/load_configuration.ts
@@ -0,0 +1,35 @@
+import { IRunEnvironment, IResolvedConfiguration } from './types'
+import { locateFile } from '../configuration/locate_file'
+import {
+ DEFAULT_CONFIGURATION,
+ fromFile,
+ IConfiguration,
+ mergeConfigurations,
+} from '../configuration'
+import { validateConfiguration } from '../configuration/validate_configuration'
+import { convertConfiguration } from './convert_configuration'
+
+export async function loadConfiguration(
+ options: {
+ file?: string
+ profiles?: string[]
+ provided?: Partial
+ },
+ { cwd = process.cwd(), env = process.env }: Partial
+): Promise {
+ const configFile = options.file ?? locateFile(cwd)
+ const profileConfiguration = configFile
+ ? await fromFile(cwd, configFile, options.profiles)
+ : {}
+ const original = mergeConfigurations(
+ DEFAULT_CONFIGURATION,
+ profileConfiguration,
+ options.provided
+ )
+ validateConfiguration(original)
+ const runnable = await convertConfiguration(original, env)
+ return {
+ original,
+ runnable,
+ }
+}
diff --git a/src/api/loadSupport.ts b/src/api/load_support.ts
similarity index 77%
rename from src/api/loadSupport.ts
rename to src/api/load_support.ts
index 5dbb29654..30aef8cd2 100644
--- a/src/api/loadSupport.ts
+++ b/src/api/load_support.ts
@@ -1,11 +1,10 @@
import { IdGenerator } from '@cucumber/messages'
-import { IUserConfiguration } from '../configuration'
-import { IRunEnvironment } from './types'
+import { IRunEnvironment, IRunnableConfiguration } from './types'
import { resolvePaths } from './paths'
import { getSupportCodeLibrary } from './support'
export async function loadSupport(
- configuration: Pick,
+ configuration: Pick,
{ cwd = process.cwd() }: Partial
) {
const newId = IdGenerator.uuid()
diff --git a/src/api/paths.ts b/src/api/paths.ts
index d156cccbb..2516329a8 100644
--- a/src/api/paths.ts
+++ b/src/api/paths.ts
@@ -2,12 +2,12 @@ import { promisify } from 'util'
import glob from 'glob'
import path from 'path'
import fs from 'mz/fs'
-import { ISourcesCoordinates } from '../configuration'
+import { ISourcesCoordinates } from './types'
import { ISupportCodeCoordinates } from '../support_code_library_builder/types'
export async function resolvePaths(
cwd: string,
- sources: ISourcesCoordinates,
+ sources: Pick,
support: ISupportCodeCoordinates
): Promise<{
unexpandedFeaturePaths: string[]
diff --git a/src/api/runCucumber.ts b/src/api/run_cucumber.ts
similarity index 97%
rename from src/api/runCucumber.ts
rename to src/api/run_cucumber.ts
index 49685dc54..6d05a7d78 100644
--- a/src/api/runCucumber.ts
+++ b/src/api/run_cucumber.ts
@@ -8,8 +8,7 @@ import {
} from '../cli/helpers'
import { GherkinStreams } from '@cucumber/gherkin-streams'
import PickleFilter from '../pickle_filter'
-import { IRunConfiguration } from '../configuration'
-import { IRunEnvironment, IRunResult } from './types'
+import { IRunConfiguration, IRunEnvironment, IRunResult } from './types'
import { resolvePaths } from './paths'
import { makeRuntime } from './runtime'
import { initializeFormatters } from './formatters'
diff --git a/src/api/runCucumber_spec.ts b/src/api/run_cucumber_spec.ts
similarity index 72%
rename from src/api/runCucumber_spec.ts
rename to src/api/run_cucumber_spec.ts
index d968ea601..b1880abf1 100644
--- a/src/api/runCucumber_spec.ts
+++ b/src/api/run_cucumber_spec.ts
@@ -4,10 +4,10 @@ import path from 'path'
import { reindent } from 'reindent-template-literals'
import { PassThrough } from 'stream'
import { expect } from 'chai'
-import { IUserConfiguration } from '../configuration'
-import { runCucumber } from './runCucumber'
+import { runCucumber } from './run_cucumber'
import { IRunEnvironment } from './types'
-import { loadSupport } from './loadSupport'
+import { loadSupport } from './load_support'
+import { loadConfiguration } from './load_configuration'
const newId = IdGenerator.uuid()
@@ -27,6 +27,10 @@ async function setupEnvironment(): Promise> {
Given('a step', function () {})
Then('another step', function () {})`)
)
+ await fs.writeFile(
+ path.join(cwd, 'cucumber.mjs'),
+ `export default {paths: ['features/test.feature'], requireModule: ['ts-node/register'], require: ['features/steps.ts']}`
+ )
const stdout = new PassThrough()
return { cwd, stdout }
}
@@ -46,21 +50,10 @@ describe('runCucumber', () => {
it('should be able to load support code upfront and supply it to runCucumber', async () => {
const messages: Envelope[] = []
- const configuration: IUserConfiguration = {
- sources: {
- paths: ['features/test.feature'],
- },
- support: {
- requireModules: ['ts-node/register'],
- requirePaths: ['features/steps.ts'],
- importPaths: [],
- },
- }
- const support = await loadSupport(configuration, environment)
- await runCucumber(
- { ...configuration, support },
- environment,
- (envelope) => messages.push(envelope)
+ const { runnable } = await loadConfiguration({}, environment)
+ const support = await loadSupport(runnable, environment)
+ await runCucumber({ ...runnable, support }, environment, (envelope) =>
+ messages.push(envelope)
)
const testStepFinishedEnvelopes = messages.filter(
(envelope) => envelope.testStepFinished
@@ -85,25 +78,12 @@ describe('runCucumber', () => {
it('successfully executes 2 test runs', async () => {
const messages: Envelope[] = []
- const configuration: IUserConfiguration = {
- sources: {
- paths: ['features/test.feature'],
- },
- support: {
- requireModules: ['ts-node/register'],
- requirePaths: ['features/steps.ts'],
- importPaths: [],
- },
- }
- const { support } = await runCucumber(
- configuration,
- environment,
- (envelope) => messages.push(envelope)
+ const { runnable } = await loadConfiguration({}, environment)
+ const { support } = await runCucumber(runnable, environment, (envelope) =>
+ messages.push(envelope)
)
- await runCucumber(
- { ...configuration, support },
- environment,
- (envelope) => messages.push(envelope)
+ await runCucumber({ ...runnable, support }, environment, (envelope) =>
+ messages.push(envelope)
)
const testStepFinishedEnvelopes = messages.filter(
diff --git a/src/api/runtime.ts b/src/api/runtime.ts
index 00099fec3..f811637d3 100644
--- a/src/api/runtime.ts
+++ b/src/api/runtime.ts
@@ -1,13 +1,10 @@
-import Runtime, {
- DEFAULT_RUNTIME_OPTIONS,
- IRuntime,
- IRuntimeOptions,
-} from '../runtime'
+import Runtime, { IRuntime } from '../runtime'
import { EventEmitter } from 'events'
import { EventDataCollector } from '../formatter/helpers'
import { IdGenerator } from '@cucumber/messages'
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
import Coordinator from '../runtime/parallel/coordinator'
+import { IRunOptionsRuntime } from './types'
export function makeRuntime({
cwd,
@@ -20,7 +17,7 @@ export function makeRuntime({
requireModules,
requirePaths,
importPaths,
- options: { parallel = 0, ...runtimeOptions } = {},
+ options: { parallel, ...options },
}: {
cwd: string
logger: Console
@@ -32,13 +29,8 @@ export function makeRuntime({
requireModules: string[]
requirePaths: string[]
importPaths: string[]
- options: Partial & { parallel?: number }
+ options: IRunOptionsRuntime
}): IRuntime {
- // sprinkle specified runtime options over the defaults
- const options = {
- ...DEFAULT_RUNTIME_OPTIONS,
- ...runtimeOptions,
- }
if (parallel > 0) {
return new Coordinator({
cwd,
diff --git a/src/api/types.ts b/src/api/types.ts
index 7d047e093..b58238240 100644
--- a/src/api/types.ts
+++ b/src/api/types.ts
@@ -1,5 +1,47 @@
-import { ISupportCodeLibrary } from '../support_code_library_builder/types'
-import { IFormatterStream } from '../formatter'
+import {
+ ISupportCodeCoordinates,
+ ISupportCodeLibrary,
+} from '../support_code_library_builder/types'
+import { FormatOptions, IFormatterStream } from '../formatter'
+import { PickleOrder } from '../models/pickle_order'
+import { IRuntimeOptions } from '../runtime'
+import { IConfiguration } from '../configuration'
+
+export interface ISourcesCoordinates {
+ defaultDialect: string
+ paths: string[]
+ names: string[]
+ tagExpression: string
+ order: PickleOrder
+}
+
+export type IRunOptionsRuntime = IRuntimeOptions & { parallel: number }
+
+export interface IRunOptionsFormats {
+ stdout: string
+ files: Record
+ publish:
+ | {
+ url?: string
+ token?: string
+ }
+ | false
+ options: FormatOptions
+}
+
+export interface IRunnableConfiguration {
+ sources: ISourcesCoordinates
+ support: ISupportCodeCoordinates
+ runtime: IRunOptionsRuntime
+ formats: IRunOptionsFormats
+}
+
+export interface IRunConfiguration {
+ sources: ISourcesCoordinates
+ support: ISupportCodeCoordinates | ISupportCodeLibrary
+ runtime: IRunOptionsRuntime
+ formats: IRunOptionsFormats
+}
export interface IRunEnvironment {
cwd: string
@@ -8,6 +50,11 @@ export interface IRunEnvironment {
env: NodeJS.ProcessEnv
}
+export interface IResolvedConfiguration {
+ original: IConfiguration
+ runnable: IRunnableConfiguration
+}
+
export interface IRunResult {
success: boolean
support: ISupportCodeLibrary
diff --git a/src/cli/configuration_builder.ts b/src/cli/configuration_builder.ts
deleted file mode 100644
index 4b3d91d7c..000000000
--- a/src/cli/configuration_builder.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { IParsedArgv, IParsedArgvOptions } from './argv_parser'
-import OptionSplitter from './option_splitter'
-import { IRunConfiguration } from '../configuration'
-
-export async function buildConfiguration(
- fromArgv: IParsedArgv,
- env: NodeJS.ProcessEnv
-): Promise {
- const { args, options } = fromArgv
- return {
- sources: {
- paths: args,
- defaultDialect: options.language,
- names: options.name,
- tagExpression: options.tags,
- order: options.order,
- },
- support: {
- requireModules: options.requireModule,
- requirePaths: options.require,
- importPaths: options.import,
- },
- runtime: {
- dryRun: options.dryRun,
- failFast: options.failFast,
- filterStacktraces: !options.backtrace,
- parallel: options.parallel,
- retry: options.retry,
- retryTagFilter: options.retryTagFilter,
- strict: options.strict,
- worldParameters: options.worldParameters,
- },
- formats: {
- stdout: options.format.find((option) => !option.includes(':')),
- files: options.format
- .filter((option) => option.includes(':'))
- .reduce((mapped, item) => {
- const [type, target] = OptionSplitter.split(item)
- return {
- ...mapped,
- [target]: type,
- }
- }, {}),
- publish: makePublishConfig(options, env),
- options: options.formatOptions,
- },
- }
-}
-
-export function isTruthyString(s: string | undefined): boolean {
- if (s === undefined) {
- return false
- }
- return s.match(/^(false|no|0)$/i) === null
-}
-
-function isPublishing(
- options: IParsedArgvOptions,
- env: NodeJS.ProcessEnv
-): boolean {
- return (
- options.publish ||
- isTruthyString(env.CUCUMBER_PUBLISH_ENABLED) ||
- env.CUCUMBER_PUBLISH_TOKEN !== undefined
- )
-}
-
-function makePublishConfig(
- options: IParsedArgvOptions,
- env: NodeJS.ProcessEnv
-): any {
- const enabled = isPublishing(options, env)
- if (!enabled) {
- return false
- }
- return {
- url: env.CUCUMBER_PUBLISH_URL,
- token: env.CUCUMBER_PUBLISH_TOKEN,
- }
-}
diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts
index a1211338e..1a1290d8f 100644
--- a/src/cli/helpers.ts
+++ b/src/cli/helpers.ts
@@ -1,11 +1,9 @@
-import ArgvParser from './argv_parser'
-import ProfileLoader from './profile_loader'
import shuffle from 'knuth-shuffle-seeded'
import { EventEmitter } from 'events'
import PickleFilter from '../pickle_filter'
import { EventDataCollector } from '../formatter/helpers'
import { doesHaveValue } from '../value_checker'
-import OptionSplitter from './option_splitter'
+import { OptionSplitter } from '../configuration'
import { Readable } from 'stream'
import os from 'os'
import * as messages from '@cucumber/messages'
@@ -14,30 +12,10 @@ import detectCiEnvironment from '@cucumber/ci-environment'
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
import TestCaseHookDefinition from '../models/test_case_hook_definition'
import TestRunHookDefinition from '../models/test_run_hook_definition'
+import { PickleOrder } from '../models/pickle_order'
import { builtinParameterTypes } from '../support_code_library_builder'
import { version } from '../version'
-export interface IGetExpandedArgvRequest {
- argv: string[]
- cwd: string
-}
-
-export async function getExpandedArgv({
- argv,
- cwd,
-}: IGetExpandedArgvRequest): Promise {
- const { options } = ArgvParser.parse(argv)
- let fullArgv = argv
- const profileArgv = await new ProfileLoader(cwd).getArgv(
- options.profile,
- options.config
- )
- if (profileArgv.length > 0) {
- fullArgv = argv.slice(0, 2).concat(profileArgv).concat(argv.slice(2))
- }
- return fullArgv
-}
-
interface IParseGherkinMessageStreamRequest {
logger: Console
eventBroadcaster: EventEmitter
@@ -47,8 +25,6 @@ interface IParseGherkinMessageStreamRequest {
pickleFilter: PickleFilter
}
-export type PickleOrder = 'defined' | 'random'
-
export async function parseGherkinMessageStream({
logger,
eventBroadcaster,
diff --git a/src/cli/helpers_spec.ts b/src/cli/helpers_spec.ts
index eda3a430b..a412700b8 100644
--- a/src/cli/helpers_spec.ts
+++ b/src/cli/helpers_spec.ts
@@ -4,7 +4,6 @@ import {
emitMetaMessage,
emitSupportCodeMessages,
parseGherkinMessageStream,
- PickleOrder,
} from './helpers'
import { EventEmitter } from 'events'
import PickleFilter from '../pickle_filter'
@@ -23,6 +22,7 @@ import {
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
import TestCaseHookDefinition from '../models/test_case_hook_definition'
import TestRunHookDefinition from '../models/test_run_hook_definition'
+import { PickleOrder } from '../models/pickle_order'
const noopFunction = (): void => {
// no code
diff --git a/src/cli/index.ts b/src/cli/index.ts
index 9ebead46b..459ebe92e 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -1,10 +1,8 @@
-import { getExpandedArgv } from './helpers'
-import { validateInstall } from './install_validator'
-import { buildConfiguration, isTruthyString } from './configuration_builder'
+import { ArgvParser, isTruthyString } from '../configuration'
import { IFormatterStream } from '../formatter'
-import { runCucumber } from '../api'
-import ArgvParser from './argv_parser'
+import { loadConfiguration, runCucumber } from '../api'
import { getKeywords, getLanguages } from './i18n'
+import { validateInstall } from './install_validator'
export interface ICliRunResult {
shouldAdvertisePublish: boolean
@@ -41,13 +39,10 @@ export default class Cli {
async run(): Promise {
await validateInstall(this.cwd)
- const fromArgv = ArgvParser.parse(
- await getExpandedArgv({
- argv: this.argv,
- cwd: this.cwd,
- })
+ const { options, configuration: argvConfiguration } = ArgvParser.parse(
+ this.argv
)
- if (fromArgv.options.i18nLanguages) {
+ if (options.i18nLanguages) {
this.stdout.write(getLanguages())
return {
shouldAdvertisePublish: false,
@@ -55,27 +50,35 @@ export default class Cli {
success: true,
}
}
- if (fromArgv.options.i18nKeywords != '') {
- this.stdout.write(getKeywords(fromArgv.options.i18nKeywords))
+ if (options.i18nKeywords) {
+ this.stdout.write(getKeywords(options.i18nKeywords))
return {
shouldAdvertisePublish: false,
shouldExitImmediately: true,
success: true,
}
}
- const configuration = await buildConfiguration(fromArgv, this.env)
- const { success } = await runCucumber(configuration, {
+ const environment = {
cwd: this.cwd,
stdout: this.stdout,
stderr: this.stderr,
env: this.env,
- })
+ }
+ const { original: configuration, runnable } = await loadConfiguration(
+ {
+ file: options.config,
+ profiles: options.profile,
+ provided: argvConfiguration,
+ },
+ environment
+ )
+ const { success } = await runCucumber(runnable, environment)
return {
shouldAdvertisePublish:
- !configuration.formats.publish &&
- !fromArgv.options.publishQuiet &&
+ !runnable.formats.publish &&
+ !configuration.publishQuiet &&
!isTruthyString(this.env.CUCUMBER_PUBLISH_QUIET),
- shouldExitImmediately: fromArgv.options.exit,
+ shouldExitImmediately: configuration.forceExit,
success,
}
}
diff --git a/src/cli/profile_loader.ts b/src/cli/profile_loader.ts
deleted file mode 100644
index 8cd97d499..000000000
--- a/src/cli/profile_loader.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import fs from 'mz/fs'
-import path from 'path'
-import stringArgv from 'string-argv'
-import { doesHaveValue, doesNotHaveValue } from '../value_checker'
-
-const DEFAULT_FILENAMES = ['cucumber.cjs', 'cucumber.js']
-
-export default class ProfileLoader {
- constructor(private readonly directory: string) {}
-
- async getDefinitions(configFile?: string): Promise> {
- if (configFile) {
- return this.loadFile(configFile)
- }
-
- const defaultFile = DEFAULT_FILENAMES.find((filename) =>
- fs.existsSync(path.join(this.directory, filename))
- )
-
- if (defaultFile) {
- return this.loadFile(defaultFile)
- }
-
- return {}
- }
-
- loadFile(configFile: string): Record {
- const definitionsFilePath: string = path.join(this.directory, configFile)
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const definitions = require(definitionsFilePath)
- if (typeof definitions !== 'object') {
- throw new Error(`${definitionsFilePath} does not export an object`)
- }
- return definitions
- }
-
- async getArgv(profiles: string[], configFile?: string): Promise {
- const definitions = await this.getDefinitions(configFile)
- if (profiles.length === 0 && doesHaveValue(definitions.default)) {
- profiles = ['default']
- }
- const argvs = profiles.map((profile) => {
- if (doesNotHaveValue(definitions[profile])) {
- throw new Error(`Undefined profile: ${profile}`)
- }
- return stringArgv(definitions[profile])
- })
- return argvs.flat()
- }
-}
diff --git a/src/cli/profile_loader_spec.ts b/src/cli/profile_loader_spec.ts
deleted file mode 100644
index fcbe4c035..000000000
--- a/src/cli/profile_loader_spec.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import { describe, it } from 'mocha'
-import { expect } from 'chai'
-import fs from 'mz/fs'
-import path from 'path'
-import ProfileLoader from './profile_loader'
-import tmp, { DirOptions } from 'tmp'
-import { promisify } from 'util'
-import { doesHaveValue, valueOrDefault } from '../value_checker'
-
-interface TestProfileLoaderOptions {
- definitionsFileContent?: string
- definitionsFileName?: string
- profiles?: string[]
- configOption?: string
-}
-
-async function testProfileLoader(
- opts: TestProfileLoaderOptions = {}
-): Promise {
- const cwd = await promisify(tmp.dir)({
- unsafeCleanup: true,
- })
- const definitionsFileName = opts.definitionsFileName ?? 'cucumber.js'
-
- if (doesHaveValue(opts.definitionsFileContent)) {
- await fs.writeFile(
- path.join(cwd, definitionsFileName),
- opts.definitionsFileContent
- )
- }
-
- const profileLoader = new ProfileLoader(cwd)
- return await profileLoader.getArgv(
- valueOrDefault(opts.profiles, []),
- opts.configOption
- )
-}
-
-describe('ProfileLoader', () => {
- describe('getArgv', () => {
- describe('with no identifiers', () => {
- describe('no definition file', () => {
- it('returns an empty array', async function () {
- // Arrange
-
- // Act
- const result = await testProfileLoader()
-
- // Assert
- expect(result).to.eql([])
- })
- })
-
- describe('with definition file', () => {
- describe('with a default', () => {
- it('returns the argv for the default profile', async function () {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {default: "--opt1 --opt2"}'
-
- // Act
- const result = await testProfileLoader({ definitionsFileContent })
-
- // Assert
- expect(result).to.eql(['--opt1', '--opt2'])
- })
- })
-
- describe('without a default', () => {
- it('returns an empty array', async function () {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {profile1: "--opt1 --opt2"}'
-
- // Act
- const result = await testProfileLoader({ definitionsFileContent })
-
- // Assert
- expect(result).to.eql([])
- })
- })
- })
- })
-
- describe('with identifiers', () => {
- describe('no definition file', () => {
- it('throws', async function () {
- // Arrange
- let caughtErrorMessage = ''
-
- // Act
- try {
- await testProfileLoader({ profiles: ['profile1'] })
- } catch (error) {
- caughtErrorMessage = error.message
- }
-
- // Assert
- expect(caughtErrorMessage).to.eql('Undefined profile: profile1')
- })
- })
-
- describe('with definition file', () => {
- describe('profile is defined', () => {
- it('returns the argv for the given profile', async function () {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {profile1: "--opt1 --opt2"}'
-
- // Act
- const result = await testProfileLoader({
- definitionsFileContent,
- profiles: ['profile1'],
- })
-
- // Assert
- expect(result).to.eql(['--opt1', '--opt2'])
- })
- })
-
- describe('profile is defined and contains quoted string', () => {
- it('returns the argv for the given profile', async function () {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {profile1: "--opt3 \'some value\'"}'
-
- // Act
- const result = await testProfileLoader({
- definitionsFileContent,
- profiles: ['profile1'],
- })
-
- // Assert
- expect(result).to.eql(['--opt3', 'some value'])
- })
- })
-
- describe('profile is not defined', () => {
- it('throws', async function () {
- // Arrange
- let caughtErrorMessage = ''
- const definitionsFileContent =
- 'module.exports = {profile1: "--opt1 --opt2"}'
-
- // Act
- try {
- await testProfileLoader({
- definitionsFileContent,
- profiles: ['profile2'],
- })
- } catch (error) {
- caughtErrorMessage = error.message
- }
-
- // Assert
- expect(caughtErrorMessage).to.eql('Undefined profile: profile2')
- })
- })
- })
- })
-
- describe('with non-default configuration file', () => {
- it('returns the argv for the given profile', async function () {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {profile3: "--opt3 --opt4"}'
-
- // Act
- const result = await testProfileLoader({
- definitionsFileContent,
- definitionsFileName: '.cucumber-rc.js',
- profiles: ['profile3'],
- configOption: '.cucumber-rc.js',
- })
-
- // Assert
- expect(result).to.eql(['--opt3', '--opt4'])
- })
-
- it('throws when the file doesnt exist', async () => {
- // Arrange
- const definitionsFileContent =
- 'module.exports = {profile3: "--opt3 --opt4"}'
-
- // Act
- try {
- await testProfileLoader({
- definitionsFileContent,
- profiles: [],
- configOption: 'doesntexist.js',
- })
- expect.fail('should throw')
- } catch (e) {
- expect(e.message).to.contain('Cannot find module')
- }
- })
- })
- })
-})
diff --git a/src/cli/argv_parser.ts b/src/configuration/argv_parser.ts
similarity index 58%
rename from src/cli/argv_parser.ts
rename to src/configuration/argv_parser.ts
index 00f738bc2..5ccb45128 100644
--- a/src/cli/argv_parser.ts
+++ b/src/configuration/argv_parser.ts
@@ -1,64 +1,36 @@
import { Command } from 'commander'
+import merge from 'lodash.merge'
import path from 'path'
import { dialects } from '@cucumber/gherkin'
-import { SnippetInterface } from '../formatter/step_definition_snippet_builder/snippet_syntax'
import Formatters from '../formatter/helpers/formatters'
import { version } from '../version'
-import { PickleOrder } from './helpers'
-
-export interface IParsedArgvFormatRerunOptions {
- separator?: string
-}
-
-export interface IParsedArgvFormatOptions {
- colorsEnabled?: boolean
- rerun?: IParsedArgvFormatRerunOptions
- snippetInterface?: SnippetInterface
- snippetSyntax?: string
- printAttachments?: boolean
- [customKey: string]: any
-}
+import { IConfiguration } from './types'
export interface IParsedArgvOptions {
- backtrace: boolean
- config: string
- dryRun: boolean
- exit: boolean
- failFast: boolean
- format: string[]
- formatOptions: IParsedArgvFormatOptions
- i18nKeywords: string
- i18nLanguages: boolean
- import: string[]
- language: string
- name: string[]
- order: PickleOrder
- parallel: number
+ config?: string
+ i18nKeywords?: string
+ i18nLanguages?: boolean
profile: string[]
- publish: boolean
- publishQuiet: boolean
- require: string[]
- requireModule: string[]
- retry: number
- retryTagFilter: string
- strict: boolean
- tags: string
- worldParameters: object
}
export interface IParsedArgv {
- args: string[]
options: IParsedArgvOptions
+ configuration: Partial
}
+type IRawArgvOptions = Partial> &
+ IParsedArgvOptions
+
const ArgvParser = {
- collect(val: T, memo: T[]): T[] {
- memo.push(val)
- return memo
+ collect(val: T, memo: T[] = []): T[] {
+ if (val) {
+ return [...memo, val]
+ }
+ return undefined
},
- mergeJson(option: string): (str: string, memo: object) => object {
- return function (str: string, memo: object) {
+ mergeJson(option: string): (str: string, memo?: object) => object {
+ return function (str: string, memo: object = {}) {
let val: object
try {
val = JSON.parse(str)
@@ -69,12 +41,12 @@ const ArgvParser = {
if (typeof val !== 'object' || Array.isArray(val)) {
throw new Error(`${option} must be passed JSON of an object: ${str}`)
}
- return { ...memo, ...val }
+ return merge(memo, val)
}
},
- mergeTags(value: string, memo: string): string {
- return memo === '' ? `(${value})` : `${memo} and (${value})`
+ mergeTags(value: string, memo?: string): string {
+ return memo ? `${memo} and (${value})` : `(${value})`
},
validateCountOption(value: string, optionName: string): number {
@@ -92,14 +64,6 @@ const ArgvParser = {
return value
},
- validateRetryOptions(options: IParsedArgvOptions): void {
- if (options.retryTagFilter !== '' && options.retry === 0) {
- throw new Error(
- 'a positive --retry count must be specified when setting --retry-tag-filter'
- )
- }
- },
-
parse(argv: string[]): IParsedArgv {
const program = new Command(path.basename(argv[1]))
@@ -108,60 +72,48 @@ const ArgvParser = {
.usage('[options] [...]')
.version(version, '-v, --version')
.option('-b, --backtrace', 'show full backtrace for errors')
- .option('-c, --config ', 'specify configuration file')
+ .option('-c, --config ', 'specify configuration file')
+ .option('-d, --dry-run', 'invoke formatters without executing steps')
.option(
- '-d, --dry-run',
- 'invoke formatters without executing steps',
- false
+ '--exit, --force-exit',
+ 'force shutdown of the event loop when the test run has finished: cucumber will call process.exit'
)
- .option(
- '--exit',
- 'force shutdown of the event loop when the test run has finished: cucumber will call process.exit',
- false
- )
- .option('--fail-fast', 'abort the run on first failure', false)
+ .option('--fail-fast', 'abort the run on first failure')
.option(
'-f, --format ',
'specify the output format, optionally supply PATH to redirect formatter output (repeatable). Available formats:\n' +
Formatters.buildFormattersDocumentationString(),
- ArgvParser.collect,
- []
+ ArgvParser.collect
)
.option(
'--format-options ',
'provide options for formatters (repeatable)',
- ArgvParser.mergeJson('--format-options'),
- {}
+ ArgvParser.mergeJson('--format-options')
)
.option(
'--i18n-keywords ',
'list language keywords',
- ArgvParser.validateLanguage,
- ''
+ ArgvParser.validateLanguage
)
- .option('--i18n-languages', 'list languages', false)
+ .option('--i18n-languages', 'list languages')
.option(
- '--import ',
+ '-i, --import ',
'import files before executing features (repeatable)',
- ArgvParser.collect,
- []
+ ArgvParser.collect
)
.option(
'--language ',
- 'provide the default language for feature files',
- 'en'
+ 'provide the default language for feature files'
)
.option(
'--name ',
'only execute the scenarios with name matching the expression (repeatable)',
- ArgvParser.collect,
- []
+ ArgvParser.collect
)
- .option('--no-strict', 'succeed even if there are pending steps')
+
.option(
'--order ',
- 'run scenarios in the specified order. Type should be `defined` or `random`',
- 'defined'
+ 'run scenarios in the specified order. Type should be `defined` or `random`'
)
.option(
'-p, --profile ',
@@ -172,55 +124,45 @@ const ArgvParser = {
.option(
'--parallel ',
'run in parallel with the given number of workers',
- (val) => ArgvParser.validateCountOption(val, '--parallel'),
- 0
- )
- .option(
- '--publish',
- 'Publish a report to https://reports.cucumber.io',
- false
+ (val) => ArgvParser.validateCountOption(val, '--parallel')
)
+ .option('--publish', 'Publish a report to https://reports.cucumber.io')
.option(
'--publish-quiet',
- "Don't print information banner about publishing reports",
- false
+ "Don't print information banner about publishing reports"
)
.option(
'-r, --require ',
'require files before executing features (repeatable)',
- ArgvParser.collect,
- []
+ ArgvParser.collect
)
.option(
'--require-module ',
'require node modules before requiring files (repeatable)',
- ArgvParser.collect,
- []
+ ArgvParser.collect
)
.option(
'--retry ',
'specify the number of times to retry failing test cases (default: 0)',
- (val) => ArgvParser.validateCountOption(val, '--retry'),
- 0
+ (val) => ArgvParser.validateCountOption(val, '--retry')
)
.option(
'--retry-tag-filter ',
`only retries the features or scenarios with tags matching the expression (repeatable).
This option requires '--retry' to be specified.`,
- ArgvParser.mergeTags,
- ''
+ ArgvParser.mergeTags
)
+ .option('--strict', 'fail if there are pending steps')
+ .option('--no-strict', 'succeed even if there are pending steps')
.option(
'-t, --tags ',
'only execute the features or scenarios with tags matching the expression (repeatable)',
- ArgvParser.mergeTags,
- ''
+ ArgvParser.mergeTags
)
.option(
'--world-parameters ',
'provide parameters that will be passed to the world constructor (repeatable)',
- ArgvParser.mergeJson('--world-parameters'),
- {}
+ ArgvParser.mergeJson('--world-parameters')
)
program.addHelpText(
@@ -229,12 +171,26 @@ const ArgvParser = {
)
program.parse(argv)
- const options: IParsedArgvOptions = program.opts()
- ArgvParser.validateRetryOptions(options)
+ const {
+ config,
+ i18nKeywords,
+ i18nLanguages,
+ profile,
+ ...regularStuff
+ }: IRawArgvOptions = program.opts()
+ const configuration: Partial = regularStuff
+ if (program.args.length > 0) {
+ configuration.paths = program.args
+ }
return {
- options,
- args: program.args,
+ options: {
+ config,
+ i18nKeywords,
+ i18nLanguages,
+ profile,
+ },
+ configuration,
}
},
}
diff --git a/src/configuration/argv_parser_spec.ts b/src/configuration/argv_parser_spec.ts
new file mode 100644
index 000000000..459e3c1dd
--- /dev/null
+++ b/src/configuration/argv_parser_spec.ts
@@ -0,0 +1,57 @@
+import { expect } from 'chai'
+import ArgvParser from './argv_parser'
+
+const baseArgv = ['/path/to/node', '/path/to/cucumber-js']
+
+describe('ArgvParser', () => {
+ describe('parse', () => {
+ it('should produce an empty object when no arguments', () => {
+ const { configuration } = ArgvParser.parse(baseArgv)
+ expect(configuration).to.deep.eq({})
+ })
+
+ it('should handle repeatable arguments', () => {
+ const { configuration } = ArgvParser.parse([
+ ...baseArgv,
+ 'features/hello.feature',
+ 'features/world.feature',
+ '--require',
+ 'hooks/**/*.js',
+ '--require',
+ 'steps/**/*.js',
+ ])
+ expect(configuration).to.deep.eq({
+ paths: ['features/hello.feature', 'features/world.feature'],
+ require: ['hooks/**/*.js', 'steps/**/*.js'],
+ })
+ })
+
+ it('should handle mergeable tag strings', () => {
+ const { configuration } = ArgvParser.parse([
+ ...baseArgv,
+ '--tags',
+ '@foo',
+ '--tags',
+ '@bar',
+ ])
+ expect(configuration).to.deep.eq({
+ tags: '(@foo) and (@bar)',
+ })
+ })
+
+ it('should handle mergeable json objects', () => {
+ const params1 = { foo: 1, bar: { stuff: 3 } }
+ const params2 = { foo: 2, bar: { things: 4 } }
+ const { configuration } = ArgvParser.parse([
+ ...baseArgv,
+ '--world-parameters',
+ JSON.stringify(params1),
+ '--world-parameters',
+ JSON.stringify(params2),
+ ])
+ expect(configuration).to.deep.eq({
+ worldParameters: { foo: 2, bar: { stuff: 3, things: 4 } },
+ })
+ })
+ })
+})
diff --git a/src/configuration/check_schema.ts b/src/configuration/check_schema.ts
new file mode 100644
index 000000000..758704eba
--- /dev/null
+++ b/src/configuration/check_schema.ts
@@ -0,0 +1,35 @@
+import * as yup from 'yup'
+import { dialects } from '@cucumber/gherkin'
+import { IConfiguration } from './types'
+
+const schema = yup.object().shape({
+ backtrace: yup.boolean(),
+ dryRun: yup.boolean(),
+ exit: yup.boolean(),
+ failFast: yup.boolean(),
+ format: yup.array().of(yup.string()),
+ formatOptions: yup.object(),
+ import: yup.array().of(yup.string()),
+ language: yup.string().oneOf(Object.keys(dialects)),
+ name: yup.array().of(yup.string()),
+ order: yup.string().oneOf(['defined', 'random']),
+ paths: yup.array().of(yup.string()),
+ parallel: yup.number().integer().min(0),
+ publish: yup.boolean(),
+ publishQuiet: yup.boolean(),
+ require: yup.array().of(yup.string()),
+ requireModule: yup.array().of(yup.string()),
+ retry: yup.number().integer().min(0),
+ retryTagFilter: yup.string(),
+ strict: yup.boolean(),
+ tags: yup.string(),
+ worldParameters: yup.object(),
+})
+
+export function checkSchema(configuration: any): Partial {
+ return schema.validateSync(configuration, {
+ abortEarly: false,
+ strict: true,
+ stripUnknown: true,
+ }) as Partial
+}
diff --git a/src/configuration/default_configuration.ts b/src/configuration/default_configuration.ts
new file mode 100644
index 000000000..4a58f28dd
--- /dev/null
+++ b/src/configuration/default_configuration.ts
@@ -0,0 +1,25 @@
+import { IConfiguration } from './types'
+
+export const DEFAULT_CONFIGURATION: IConfiguration = {
+ backtrace: false,
+ dryRun: false,
+ forceExit: false,
+ failFast: false,
+ format: [],
+ formatOptions: {},
+ import: [],
+ language: 'en',
+ name: [],
+ order: 'defined',
+ paths: [],
+ parallel: 0,
+ publish: false,
+ publishQuiet: false,
+ require: [],
+ requireModule: [],
+ retry: 0,
+ retryTagFilter: '',
+ strict: true,
+ tags: '',
+ worldParameters: {},
+}
diff --git a/src/configuration/from_file.ts b/src/configuration/from_file.ts
new file mode 100644
index 000000000..977864d67
--- /dev/null
+++ b/src/configuration/from_file.ts
@@ -0,0 +1,81 @@
+import stringArgv from 'string-argv'
+import path from 'path'
+import { pathToFileURL } from 'url'
+import { IConfiguration } from './types'
+import { mergeConfigurations } from './merge_configurations'
+import ArgvParser from './argv_parser'
+import { checkSchema } from './check_schema'
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { importer } = require('../importer')
+
+export async function fromFile(
+ cwd: string,
+ file: string,
+ profiles: string[] = []
+): Promise> {
+ const definitions = await loadFile(cwd, file)
+ if (!definitions.default) {
+ definitions.default = {}
+ }
+ if (profiles.length < 1) {
+ profiles = ['default']
+ }
+ const definedKeys = Object.keys(definitions)
+ profiles.forEach((profileKey) => {
+ if (!definedKeys.includes(profileKey)) {
+ throw new Error(`Requested profile "${profileKey}" doesn't exist`)
+ }
+ })
+ return mergeConfigurations(
+ {},
+ ...profiles.map((profileKey) =>
+ extractConfiguration(profileKey, definitions[profileKey])
+ )
+ )
+}
+
+async function loadFile(
+ cwd: string,
+ file: string
+): Promise> {
+ const filePath: string = path.join(cwd, file)
+ let definitions
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ definitions = require(filePath)
+ } catch (error) {
+ if (error.code === 'ERR_REQUIRE_ESM') {
+ definitions = await importer(pathToFileURL(filePath))
+ } else {
+ throw error
+ }
+ }
+ if (typeof definitions !== 'object') {
+ throw new Error(`Configuration file ${filePath} does not export an object`)
+ }
+ return definitions
+}
+
+function extractConfiguration(
+ name: string,
+ definition: any
+): Partial {
+ if (typeof definition === 'string') {
+ const { configuration } = ArgvParser.parse([
+ 'node',
+ 'cucumber-js',
+ ...stringArgv(definition),
+ ])
+ return configuration
+ }
+ try {
+ return checkSchema(definition)
+ } catch (error) {
+ throw new Error(
+ `Requested profile "${name}" failed schema validation: ${error.errors.join(
+ ' '
+ )}`
+ )
+ }
+}
diff --git a/src/configuration/from_file_spec.ts b/src/configuration/from_file_spec.ts
new file mode 100644
index 000000000..6b672c803
--- /dev/null
+++ b/src/configuration/from_file_spec.ts
@@ -0,0 +1,101 @@
+import { expect } from 'chai'
+import { promisify } from 'util'
+import fs from 'fs'
+import tmp, { DirOptions } from 'tmp'
+import path from 'path'
+import { fromFile } from './from_file'
+
+async function setup(
+ file: string = 'cucumber.js',
+ content: string = `module.exports = {default: {paths: ['some/path/*.feature']}, p1: {paths: ['other/path/*.feature']}, p2: 'other/other/path/*.feature --no-strict'}`
+) {
+ const cwd = await promisify(tmp.dir)({
+ unsafeCleanup: true,
+ })
+ fs.writeFileSync(path.join(cwd, file), content, { encoding: 'utf-8' })
+ return { cwd }
+}
+
+describe('fromFile', () => {
+ it('should return empty config if no default provide and no profiles requested', async () => {
+ const { cwd } = await setup(
+ 'cucumber.js',
+ `module.exports = {p1: {paths: ['other/path/*.feature']}}`
+ )
+
+ const result = await fromFile(cwd, 'cucumber.js', [])
+ expect(result).to.deep.eq({})
+ })
+
+ it('should get default config from file if no profiles requested', async () => {
+ const { cwd } = await setup()
+
+ const result = await fromFile(cwd, 'cucumber.js', [])
+ expect(result).to.deep.eq({ paths: ['some/path/*.feature'] })
+ })
+
+ it('should throw when a requested profile doesnt exist', async () => {
+ const { cwd } = await setup()
+
+ try {
+ await fromFile(cwd, 'cucumber.js', ['nope'])
+ expect.fail('should have thrown')
+ } catch (error) {
+ expect(error.message).to.eq(`Requested profile "nope" doesn't exist`)
+ }
+ })
+
+ it('should get single profile config from file', async () => {
+ const { cwd } = await setup()
+
+ const result = await fromFile(cwd, 'cucumber.js', ['p1'])
+ expect(result).to.deep.eq({ paths: ['other/path/*.feature'] })
+ })
+
+ it('should merge multiple profiles config from file', async () => {
+ const { cwd } = await setup()
+
+ const result = await fromFile(cwd, 'cucumber.js', ['p1', 'p2'])
+ expect(result).to.deep.eq({
+ paths: ['other/path/*.feature', 'other/other/path/*.feature'],
+ strict: false,
+ })
+ })
+
+ it('should throw when an object doesnt conform to the schema', async () => {
+ const { cwd } = await setup(
+ 'cucumber.js',
+ `module.exports = {p1: {paths: 4, things: 8, requireModule: 'aardvark'}}`
+ )
+ try {
+ await fromFile(cwd, 'cucumber.js', ['p1'])
+ expect.fail('should have thrown')
+ } catch (error) {
+ expect(error.message).to.eq(
+ 'Requested profile "p1" failed schema validation: paths must be a `array` type, but the final value was: `4`. requireModule must be a `array` type, but the final value was: `"aardvark"`.'
+ )
+ }
+ })
+
+ describe('other formats', () => {
+ it('should work with esm', async () => {
+ const { cwd } = await setup(
+ 'cucumber.mjs',
+ `export default {}; export const p1 = {paths: ['other/path/*.feature']}`
+ )
+
+ const result = await fromFile(cwd, 'cucumber.mjs', ['p1'])
+ expect(result).to.deep.eq({ paths: ['other/path/*.feature'] })
+ })
+
+ it('should work with json', async () => {
+ const { cwd } = await setup(
+ 'cucumber.json',
+ `{ "default": {}, "p1": { "paths": ["other/path/*.feature"] } }`
+ )
+
+ const result = await fromFile(cwd, 'cucumber.json', ['p1'])
+ expect(result).to.deep.eq({ paths: ['other/path/*.feature'] })
+ })
+ })
+})
diff --git a/src/configuration/helpers.ts b/src/configuration/helpers.ts
new file mode 100644
index 000000000..f92451410
--- /dev/null
+++ b/src/configuration/helpers.ts
@@ -0,0 +1,6 @@
+export function isTruthyString(s: string | undefined): boolean {
+ if (s === undefined) {
+ return false
+ }
+ return s.match(/^(false|no|0)$/i) === null
+}
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index c9f6f047d..4f4bd9301 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -1 +1,7 @@
+export { default as ArgvParser } from './argv_parser'
+export * from './default_configuration'
+export * from './from_file'
+export * from './helpers'
+export * from './merge_configurations'
+export * from './option_splitter'
export * from './types'
diff --git a/src/configuration/locate_file.ts b/src/configuration/locate_file.ts
new file mode 100644
index 000000000..e4897aecf
--- /dev/null
+++ b/src/configuration/locate_file.ts
@@ -0,0 +1,15 @@
+import fs from 'mz/fs'
+import path from 'path'
+
+const DEFAULT_FILENAMES = [
+ 'cucumber.js',
+ 'cucumber.cjs',
+ 'cucumber.mjs',
+ 'cucumber.json',
+]
+
+export function locateFile(cwd: string): string | undefined {
+ return DEFAULT_FILENAMES.find((filename) =>
+ fs.existsSync(path.join(cwd, filename))
+ )
+}
diff --git a/src/configuration/merge_configurations.ts b/src/configuration/merge_configurations.ts
new file mode 100644
index 000000000..13e86e79f
--- /dev/null
+++ b/src/configuration/merge_configurations.ts
@@ -0,0 +1,50 @@
+import { IConfiguration } from './types'
+import mergeWith from 'lodash.mergewith'
+
+const ADDITIVE_ARRAYS = [
+ 'format',
+ 'import',
+ 'name',
+ 'paths',
+ 'require',
+ 'requireModule',
+]
+const TAG_EXPRESSIONS = ['tags', 'retryTagFilter']
+
+function mergeArrays(objValue: any[], srcValue: any[]) {
+ if (objValue && srcValue) {
+ return [].concat(objValue, srcValue)
+ }
+ return undefined
+}
+
+function mergeTagExpressions(objValue: string, srcValue: string) {
+ if (objValue && srcValue) {
+ return `${wrapTagExpression(objValue)} and ${wrapTagExpression(srcValue)}`
+ }
+ return undefined
+}
+
+function wrapTagExpression(raw: string) {
+ if (raw.startsWith('(') && raw.endsWith(')')) {
+ return raw
+ }
+ return `(${raw})`
+}
+
+function customizer(objValue: any, srcValue: any, key: string): any {
+ if (ADDITIVE_ARRAYS.includes(key)) {
+ return mergeArrays(objValue, srcValue)
+ }
+ if (TAG_EXPRESSIONS.includes(key)) {
+ return mergeTagExpressions(objValue, srcValue)
+ }
+ return undefined
+}
+
+export function mergeConfigurations>(
+ source: T,
+ ...configurations: Partial[]
+): T {
+ return mergeWith({}, source, ...configurations, customizer)
+}
diff --git a/src/configuration/merge_configurations_spec.ts b/src/configuration/merge_configurations_spec.ts
new file mode 100644
index 000000000..b4e0730de
--- /dev/null
+++ b/src/configuration/merge_configurations_spec.ts
@@ -0,0 +1,66 @@
+import { expect } from 'chai'
+import { mergeConfigurations } from './merge_configurations'
+
+describe('mergeConfigurations', () => {
+ it('should not default anything with empty configurations', () => {
+ const result = mergeConfigurations({}, {})
+ expect(result).to.deep.eq({})
+ })
+
+ describe('additive arrays', () => {
+ it('should merge two arrays correctly', () => {
+ const result = mergeConfigurations({ paths: ['a'] }, { paths: ['b'] })
+ expect(result).to.deep.eq({
+ paths: ['a', 'b'],
+ })
+ })
+
+ it('should handle one array and one undefined correctly', () => {
+ const result = mergeConfigurations({ paths: ['a'] }, { paths: undefined })
+ expect(result).to.deep.eq({
+ paths: ['a'],
+ })
+ })
+
+ it('should handle one undefined and one array correctly', () => {
+ const result = mergeConfigurations({ paths: undefined }, { paths: ['b'] })
+ expect(result).to.deep.eq({
+ paths: ['b'],
+ })
+ })
+ })
+
+ describe('tag expressions', () => {
+ it('should merge two tag expressions correctly', () => {
+ const result = mergeConfigurations({ tags: '@foo' }, { tags: '@bar' })
+ expect(result).to.deep.eq({
+ tags: '(@foo) and (@bar)',
+ })
+ })
+
+ it('should handle one tag and one undefined correctly', () => {
+ const result = mergeConfigurations({ tags: '@foo' }, { tags: undefined })
+ expect(result).to.deep.eq({
+ tags: '@foo',
+ })
+ })
+
+ it('should handle one undefined and one tag correctly', () => {
+ const result = mergeConfigurations({ tags: undefined }, { tags: '@foo' })
+ expect(result).to.deep.eq({
+ tags: '@foo',
+ })
+ })
+
+ it('should merge three tag expressions correctly', () => {
+ const result = mergeConfigurations(
+ { tags: '@foo' },
+ { tags: '@bar' },
+ { tags: '@baz' }
+ )
+ expect(result).to.deep.eq({
+ tags: '(@foo) and (@bar) and (@baz)',
+ })
+ })
+ })
+})
diff --git a/src/cli/option_splitter.ts b/src/configuration/option_splitter.ts
similarity index 89%
rename from src/cli/option_splitter.ts
rename to src/configuration/option_splitter.ts
index 9b7371c7e..c65ad2235 100644
--- a/src/cli/option_splitter.ts
+++ b/src/configuration/option_splitter.ts
@@ -1,4 +1,4 @@
-const OptionSplitter = {
+export const OptionSplitter = {
split(option: string): string[] {
option = option.replace(/"/g, '')
@@ -23,5 +23,3 @@ const OptionSplitter = {
function partNeedsRecombined(i: number): boolean {
return i % 2 === 0
}
-
-export default OptionSplitter
diff --git a/src/cli/option_splitter_spec.ts b/src/configuration/option_splitter_spec.ts
similarity index 96%
rename from src/cli/option_splitter_spec.ts
rename to src/configuration/option_splitter_spec.ts
index 162f92e54..93e48cdb2 100644
--- a/src/cli/option_splitter_spec.ts
+++ b/src/configuration/option_splitter_spec.ts
@@ -1,6 +1,6 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
-import OptionSplitter from './option_splitter'
+import { OptionSplitter } from './option_splitter'
describe('OptionSplitter', () => {
const examples = [
diff --git a/src/configuration/types.ts b/src/configuration/types.ts
index 7fc80eeb1..208f431cd 100644
--- a/src/configuration/types.ts
+++ b/src/configuration/types.ts
@@ -1,41 +1,26 @@
-import { IRuntimeOptions } from '../runtime'
-import { IParsedArgvFormatOptions } from '../cli/argv_parser'
-import { PickleOrder } from '../cli/helpers'
-import {
- ISupportCodeCoordinates,
- ISupportCodeLibrary,
-} from '../support_code_library_builder/types'
+import { FormatOptions } from '../formatter'
+import { PickleOrder } from '../models/pickle_order'
-export interface ISourcesCoordinates {
- defaultDialect?: string
- paths?: string[]
- names?: string[]
- tagExpression?: string
- order?: PickleOrder
-}
-
-export interface IUserConfiguration {
- sources: ISourcesCoordinates
- support: ISupportCodeCoordinates
- runtime?: Partial & { parallel?: number }
- formats?: IFormatterConfiguration
-}
-
-export interface IRunConfiguration {
- sources: ISourcesCoordinates
- support: ISupportCodeCoordinates | ISupportCodeLibrary
- runtime?: Partial & { parallel?: number }
- formats?: IFormatterConfiguration
-}
-
-export interface IFormatterConfiguration {
- stdout?: string
- files?: Record
- publish?:
- | {
- url?: string
- token?: string
- }
- | false
- options?: IParsedArgvFormatOptions
+export interface IConfiguration {
+ backtrace: boolean
+ dryRun: boolean
+ forceExit: boolean
+ failFast: boolean
+ format: string[]
+ formatOptions: FormatOptions
+ import: string[]
+ language: string
+ name: string[]
+ order: PickleOrder
+ paths: string[]
+ parallel: number
+ publish: boolean
+ publishQuiet: boolean
+ require: string[]
+ requireModule: string[]
+ retry: number
+ retryTagFilter: string
+ strict: boolean
+ tags: string
+ worldParameters: any
}
diff --git a/src/configuration/validate_configuration.ts b/src/configuration/validate_configuration.ts
new file mode 100644
index 000000000..05fdbd825
--- /dev/null
+++ b/src/configuration/validate_configuration.ts
@@ -0,0 +1,9 @@
+import { IConfiguration } from './types'
+
+export function validateConfiguration(configuration: IConfiguration): void {
+ if (configuration.retryTagFilter && !configuration.retry) {
+ throw new Error(
+ 'a positive `retry` count must be specified when setting `retryTagFilter`'
+ )
+ }
+}
diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts
index 22d9fc647..3d906acd9 100644
--- a/src/formatter/builder.ts
+++ b/src/formatter/builder.ts
@@ -3,12 +3,15 @@ import JavascriptSnippetSyntax from './step_definition_snippet_builder/javascrip
import path from 'path'
import StepDefinitionSnippetBuilder from './step_definition_snippet_builder'
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
-import Formatter, { IFormatterCleanupFn, IFormatterLogFn } from '.'
+import Formatter, {
+ FormatOptions,
+ IFormatterCleanupFn,
+ IFormatterLogFn,
+} from '.'
import { doesHaveValue, doesNotHaveValue } from '../value_checker'
import { EventEmitter } from 'events'
import EventDataCollector from './helpers/event_data_collector'
import { Writable as WritableStream } from 'stream'
-import { IParsedArgvFormatOptions } from '../cli/argv_parser'
import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax'
import { pathToFileURL } from 'url'
import Formatters from './helpers/formatters'
@@ -27,7 +30,7 @@ export interface IBuildOptions {
eventBroadcaster: EventEmitter
eventDataCollector: EventDataCollector
log: IFormatterLogFn
- parsedArgvOptions: IParsedArgvFormatOptions
+ parsedArgvOptions: FormatOptions
stream: WritableStream
cleanup: IFormatterCleanupFn
supportCodeLibrary: ISupportCodeLibrary
diff --git a/src/formatter/index.ts b/src/formatter/index.ts
index 015302f02..995fb142a 100644
--- a/src/formatter/index.ts
+++ b/src/formatter/index.ts
@@ -6,9 +6,22 @@ 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 { IParsedArgvFormatOptions } from '../cli/argv_parser'
import HttpStream from './http_stream'
import { valueOrDefault } from '../value_checker'
+import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax'
+
+export interface FormatRerunOptions {
+ separator?: string
+}
+
+export interface FormatOptions {
+ colorsEnabled?: boolean
+ rerun?: FormatRerunOptions
+ snippetInterface?: SnippetInterface
+ snippetSyntax?: string
+ printAttachments?: boolean
+ [customKey: string]: any
+}
export type IFormatterStream =
| FsWriteStream
@@ -24,7 +37,7 @@ export interface IFormatterOptions {
eventBroadcaster: EventEmitter
eventDataCollector: EventDataCollector
log: IFormatterLogFn
- parsedArgvOptions: IParsedArgvFormatOptions
+ parsedArgvOptions: FormatOptions
snippetBuilder: StepDefinitionSnippetBuilder
stream: WritableStream
cleanup: IFormatterCleanupFn
diff --git a/src/models/pickle_order.ts b/src/models/pickle_order.ts
new file mode 100644
index 000000000..d3fc77e0e
--- /dev/null
+++ b/src/models/pickle_order.ts
@@ -0,0 +1 @@
+export type PickleOrder = 'defined' | 'random' | string
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index ef0f9b1ee..64d33e2f5 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -33,16 +33,6 @@ export interface IRuntimeOptions {
worldParameters: any
}
-export const DEFAULT_RUNTIME_OPTIONS: IRuntimeOptions = {
- dryRun: false,
- failFast: false,
- filterStacktraces: true,
- retry: 0,
- retryTagFilter: '',
- strict: true,
- worldParameters: {},
-}
-
export default class Runtime implements IRuntime {
private readonly eventBroadcaster: EventEmitter
private readonly eventDataCollector: EventDataCollector
diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts
index 8e2825529..3a0008854 100644
--- a/test/formatter_helpers.ts
+++ b/test/formatter_helpers.ts
@@ -9,10 +9,10 @@ import * as messages from '@cucumber/messages'
import { ISupportCodeLibrary } from '../src/support_code_library_builder/types'
import { ITestCaseAttempt } from '../src/formatter/helpers/event_data_collector'
import { doesNotHaveValue } from '../src/value_checker'
-import { IParsedArgvFormatOptions } from '../src/cli/argv_parser'
import { PassThrough } from 'stream'
import { emitSupportCodeMessages } from '../src/cli/helpers'
import { promisify } from 'util'
+import { FormatOptions } from '../src/formatter'
const { uuid } = IdGenerator
@@ -29,7 +29,7 @@ export interface ITestRunOptions {
}
export interface ITestFormatterOptions extends ITestRunOptions {
- parsedArgvOptions?: IParsedArgvFormatOptions
+ parsedArgvOptions?: FormatOptions
type: string
}