Skip to content

Commit 4e93ed9

Browse files
committed
Extend explanation of pairing
1 parent 2f31b50 commit 4e93ed9

File tree

1 file changed

+112
-9
lines changed

1 file changed

+112
-9
lines changed

docs/step-definitions.md

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,133 @@ Step definitions are resolved using search paths that are configurable through t
1212
}
1313
```
1414

15-
This means that if you have a file `cypress/e2e/duckduckgo.feature`, it will match step definitions found in
15+
The default configuration means that if you have a file `cypress/e2e/duckduckgo.feature`, it will match step definitions found in
1616

1717
* `cypress/e2e/duckduckgo/steps.ts`
1818
* `cypress/e2e/duckduckgo.ts`
1919
* `cypress/support/step_definitions/duckduckgo.ts`
2020

21-
## Hierarchy
21+
## Pairing explained
2222

23-
There's also a `[filepart]` option available. Given a configuration shown below
23+
From Cucumber you might be familiar with the fact that step definitons *aren't* linked to particular feature files, as per their [docs](https://cucumber.io/docs/cucumber/step-definitions):
24+
25+
> 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.
26+
27+
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.
28+
29+
**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 `stepDefintions` property. Below are some examples to further explain this concept.
30+
31+
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](https://github.com/isaacs/node-glob) is used.
32+
33+
### Example 1: Templating introduced
34+
35+
Let's consider the following directory structure.
36+
37+
```
38+
/home/john.doe/my-project
39+
└── cypress
40+
└── e2e
41+
├── bar
42+
│   ├── a.feature
43+
│   └── a.js
44+
└── foo
45+
├── b.feature
46+
└── b.js
47+
```
48+
49+
.. and a configuration `{ "stepDefinitions": ["cypress/e2e/[filepath].js"] }`. The following is what happens when you run `a.feature`:
50+
51+
1. 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"`.
52+
2. The *integration directory* (`"cypress/e2e"`) is subtracted from above result, resulting in `"foo/a.feature"`.
53+
- For Cypress v9 users and below, the *integration directory* is an explicitly configured path.
54+
- 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"`.
55+
3. The file extension is removed, leaving us with `"foo/a"`.
56+
57+
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](https://github.com/isaacs/node-glob)).
58+
59+
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.
60+
61+
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.
62+
63+
### Example 2: Directory with common step definitions
64+
65+
Let's now consider the following directory structure.
66+
67+
```
68+
/home/john.doe/my-project
69+
└── cypress
70+
└── e2e
71+
├── foo
72+
│   ├── a.feature
73+
│   └── a.js
74+
├── bar
75+
│   ├── b.feature
76+
│   └── b.js
77+
└── common-step-definitions
78+
├── 1.js
79+
└── 2.js
80+
```
81+
82+
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.
83+
84+
```
85+
{
86+
"stepDefinitions": [
87+
"cypress/e2e/[filepath].js",
88+
"cypress/e2e/common-step-definitions/*.js"
89+
]
90+
}
91+
```
92+
93+
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.
94+
95+
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](https://github.com/isaacs/node-glob) and would yield `1.js` and `2.js`.
96+
97+
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.
98+
99+
### Example 3: A common mistake
100+
101+
Now let's consider the *same* file & directory structure as in the previous example, but the following configuration.
102+
103+
```
104+
{
105+
"stepDefinitions": [
106+
"cypress/e2e/**/*.js"
107+
]
108+
}
109+
```
110+
111+
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](https://github.com/isaacs/node-glob), which will find *every* `.js` file in the hierarchy.
112+
113+
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.
114+
115+
### Example 4: Hierarchy
116+
117+
There's also a `[filepart]` option available (notice the subtle difference from `[filepath]`). Given a configuration shown below
24118

25119
```json
26120
{
27121
"stepDefinitions": [
28-
"[filepart]/step_definitions/**/*.{js,ts}"
122+
"cypress/e2e/[filepart]/step_definitions/**/*.{js,ts}"
29123
]
30124
}
31125
```
32126

33-
... and a feature file `cypress/e2e/foo/bar/baz.feature`, the preprocessor would look for step definitions in
127+
... 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).
128+
129+
The `[filepart]` options works similarly, except that the previous mentioned value (`"foo/bar/baz"`) is further split into
130+
131+
- `"foo/bar/baz"`
132+
- `"foo/bar"`
133+
- `"foo"`
134+
- `"."`
135+
136+
All of these will be used to replace `[filepart]`. Thus, a single configuration member would yield *four* values and use [glob](https://github.com/isaacs/node-glob) to search in
34137

35-
* `cypress/e2e/foo/bar/baz/step_definitions/**/*.{js,ts}`
36-
* `cypress/e2e/foo/bar/step_definitions/**/*.{js,ts}`
37-
* `cypress/e2e/foo/step_definitions/**/*.{js,ts}`
38-
* `cypress/e2e/step_definitions/**/*.{js,ts}`
138+
* `"cypress/e2e/foo/bar/baz/step_definitions/**/*.{js,ts}"`
139+
* `"cypress/e2e/foo/bar/step_definitions/**/*.{js,ts}"`
140+
* `"cypress/e2e/foo/step_definitions/**/*.{js,ts}"`
141+
* `"cypress/e2e/step_definitions/**/*.{js,ts}"`
39142

40143
## Caveats
41144

0 commit comments

Comments
 (0)