Step definitions are resolved using search paths that are configurable through the stepDefinitions
property. The preprocessor uses cosmiconfig, which means you can place configuration options in EG. .cypress-cucumber-preprocessorrc.json
or package.json
. The default (almost true1) search paths are shown below.
{
"stepDefinitions": [
"cypress/e2e/[filepath]/**/*.{js,ts}",
"cypress/e2e/[filepath].{js,ts}",
"cypress/support/step_definitions/**/*.{js,ts}",
]
}
The default configuration means that if you have a file cypress/e2e/duckduckgo.feature
, it will match step definitions found in
cypress/e2e/duckduckgo/steps.ts
cypress/e2e/duckduckgo.ts
cypress/support/step_definitions/duckduckgo.ts
From Cucumber you might be familiar with the fact that step definitons aren't linked to particular feature files, as per their docs:
Step definitions aren’t linked to a particular feature file or scenario. The file, class or package name of a step definition does not affect what Gherkin steps it will match. The only thing that matters is the step definition’s expression.
This is not true for the preprocessor and represents the only place where the two experiences (Cucumber vs. Cypress + this preprocessor) deviates significantly. However, the preprocessor don't magically understand your intention in regards to this.
Pairing dictates which step definitions will be available in which feature files. Furthermore, hooks specified in paired files are the hooks which will take effect. Pairing is entirely configured through the stepDefinitions
property. Below are some examples to further explain this concept.
A prerequisite for understanding this process is some knowledge of glob / search patterns. Explaining this however is out of the scope is this page. Internally, glob is used.
Let's consider the following directory structure.
/home/john.doe/my-project
└── cypress
└── e2e
├── foo
│ ├── a.feature
│ └── a.js
└── bar
├── b.feature
└── b.js
.. and a configuration { "stepDefinitions": ["cypress/e2e/[filepath].js"] }
. The following is what happens when you run a.feature
:
- The project root (
"/home/john.doe/my-project"
) is subtracted from the full path of the feature file, leaving us with with"cypress/e2e/foo/a.feature"
. - The integration directory (
"cypress/e2e"
) is subtracted from above result, resulting in"foo/a.feature"
.- For Cypress v9 users and below, the integration directory is an explicitly configured path.
- For Cypress v10 users and higher, this is implicitly calculated and is the common ancestor path of all feature files found. For the example above, this is
"cypress/e2e"
. Thus we're left with"foo/a.feature"
.
- The file extension is removed, leaving us with
"foo/a"
.
The last value is used to replace [filepath]
in each member of the configured stepDefinitions
. For our example above this would yield ["cypress/e2e/foo/a.js"]
. This resulting array is used as search patterns for files containing step definitions (internally using glob).
When a.feature
is run, the preprocessor will only looks for step definitions in cypress/e2e/foo/a.js
. Furthermore, only hooks defined in said file will take effect. The feature file is now said to be paired with that file containing step definitions.
The observant reader will now understand that the [filepath]
template value allows you to create a hierarchy of step definitions that matches the structure of your feature files. It also allows to you put step definitions in an entirely separate directory.
Let's now consider the following directory structure.
/home/john.doe/my-project
└── cypress
└── e2e
├── foo
│ ├── a.feature
│ └── a.js
├── bar
│ ├── b.feature
│ └── b.js
└── common-step-definitions
├── 1.js
└── 2.js
Using the same value stepDefinitions
as before, the preprocessor would not be able to resolve any files in cypress/e2e/common-step-definitions
. This is likely not what you want, but we can add a member to the configuration value, as shown below.
{
"stepDefinitions": [
"cypress/e2e/[filepath].js",
"cypress/e2e/common-step-definitions/*.js"
]
}
When running a.feature
, we know from the previous example that the first member of the array would yield the search pattern cypress/e2e/foo/a.js
, which correctly matches one of our files.
The least member however, obeys to the same rules and goes to the same string replacement process described previously. It doesn't contain [filepath]
, but that doesn't matter. The resulting search pattern is "cypress/e2e/common-step-definitions/*.js"
, which is provided to glob and would yield 1.js
and 2.js
.
All of these three files are now said to be paired with a.feature
. Hooks in b.js
will not be available when running a.feature
and its hooks will not take effect.
Now let's consider the same file & directory structure as in the previous example, but the following configuration.
{
"stepDefinitions": [
"cypress/e2e/**/*.js"
]
}
The single value will go through the string replacement process and yield itself, because it does not contain the string [filepath]
. The value will then be propagated to glob, which will find every .js
file in the hierarchy.
You might initially be tempted to believe that you have configured the processor correctly, because your test might run at this point. However, step definitions from b.js
will be available in a.feature
and vice versa. Similarly, hooks in either file will always take effect.
There's also a [filepart]
option available (notice the subtle difference from [filepath]
). Given a configuration shown below
{
"stepDefinitions": [
"cypress/e2e/[filepart]/step_definitions/**/*.{js,ts}"
]
}
... and a feature file cypress/e2e/foo/bar/baz.feature
. When running said feature file, the preprocessor would go through a process and replace [filepath]
with "foo/bar/baz"
in each member of the configuration (here a single value).
The [filepart]
options works similarly, except that the previous mentioned value ("foo/bar/baz"
) is further split into
"foo/bar/baz"
"foo/bar"
"foo"
"."
All of these will be used to replace [filepart]
. Thus, a single configuration member would yield four values and use glob to search in
"cypress/e2e/foo/bar/baz/step_definitions/**/*.{js,ts}"
"cypress/e2e/foo/bar/step_definitions/**/*.{js,ts}"
"cypress/e2e/foo/step_definitions/**/*.{js,ts}"
"cypress/e2e/step_definitions/**/*.{js,ts}"
The library makes use of glob to turn patterns into a list of files. This has some implications, explained below (list is non-exhaustive and will be expanded on demand).
- Single term inside braces, EG.
foo/bar.{js}
, doesn't work as you might expect, ref. isaacs/node-glob#434.
Footnotes
-
I claim that the default configuration is of that below and this is almost always true.
{ "stepDefinitions": [ "cypress/e2e/[filepath]/**/*.{js,ts}", "cypress/e2e/[filepath].{js,ts}", "cypress/support/step_definitions/**/*.{js,ts}", ] }
The reality is a bit more complex, in order to support a variety of use-cases out-of-the-box. Most users will place their feature files in
cypress/e2e
and in those cases, the default configuration will be that of my claim.However, users that decide on a different location, let's say.
cypress/features
- in their case, the default behavior of the preprocessor will be as if you configuredstepDefinitions
shown like below.{ "stepDefinitions": [ "cypress/features/[filepath]/**/*.{js,ts}", "cypress/features/[filepath].{js,ts}", "cypress/support/step_definitions/**/*.{js,ts}", ] }
Essentially, the first part of the two first elements, is determined finding out the common ancestor path of all your feature files and thus the default configuration is dynamic. ↩