@g_package/jest-cucumber-fusion 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,43 +3,41 @@
3
3
  Write Cucumber feature files and run them as Jest tests, without the scaffolding.
4
4
 
5
5
  [![Build Status](https://github.com/gotreasa/jest-cucumber-fusion/workflows/Continuous%20Integration/badge.svg)](https://github.com/gotreasa/jest-cucumber-fusion/actions?query=workflow%3A%22Continuous+Integration%22)
6
- [![Codecov](https://codecov.io/gh/gotreasa/jest-cucumber-fusion/branch/master/graph/badge.svg)](https://codecov.io/gh/gotreasa/jest-cucumber-fusion)
6
+ [![Codecov](https://codecov.io/gh/gotreasa/jest-cucumber-fusion/branch/main/graph/badge.svg)](https://codecov.io/gh/gotreasa/jest-cucumber-fusion)
7
7
 
8
8
  [![npm downloads](https://img.shields.io/npm/dm/@g_package/jest-cucumber-fusion)](https://www.npmjs.com/package/@g_package/jest-cucumber-fusion)
9
9
  [![npm latest](https://img.shields.io/npm/v/@g_package/jest-cucumber-fusion/latest.svg)](https://www.npmjs.com/package/@g_package/jest-cucumber-fusion)
10
10
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
11
11
 
12
-
13
12
  ## Overview
14
- Jest Cucumber Fusion runs your Cucumber feature files as Jest tests. You write the feature file in Gherkin, and a step definition file with a `Given`, `When`, `Then`, `And` or `But` definition for each step. A `Fusion` call at the end of the step definition file links the two: Fusion reads the feature and creates one Jest test per scenario, so you write no `describe` or `it` blocks yourself. Everything else is plain Jest: `expect`, mocks, coverage and reporting.
15
-
16
- The style of this package began with [Jest-cucumber](https://github.com/bencompton/jest-cucumber), which it was originally built on top of. Since version 3 it no longer depends on that package: it runs on Jest and [@cucumber/gherkin](https://github.com/cucumber/gherkin) directly, and owns the whole test lifecycle itself.
17
13
 
18
- This package continues [b-yond-infinite-network/jest-cucumber-fusion](https://github.com/b-yond-infinite-network/jest-cucumber-fusion), published on npm as `jest-cucumber-fusion` until version 0.8.1 in June 2021, and is now maintained and published as `@g_package/jest-cucumber-fusion`.
19
-
20
- ## Motivation
14
+ Jest Cucumber Fusion runs your Cucumber feature files as Jest tests. You write the feature file in Gherkin, and a step definition file with a `Given`, `When`, `Then`, `And` or `But` definition for each step. A `Fusion` call at the end of the step definition file links the two: Fusion reads the feature and creates one Jest test per scenario, so you write no `describe` or `it` blocks yourself. Everything else is plain Jest: `expect`, mocks, coverage and reporting.
21
15
 
22
- Jest-cucumber has you write a `defineFeature` block, a `test` for every scenario and a callback for every step to link Jest and Cucumber. With Jest Cucumber Fusion you write only:
23
- - a Cucumber Feature file with gherkin sentences
24
- - a Cucumber Step definition file with your javascript validation code, ended with the `Fusion` function to link the two
16
+ **New to Gherkin?** A feature file describes behaviour in plain sentences. Each feature file holds one `Feature`, and each `Scenario` in it is one example of that behaviour. Each line of a scenario starting with `Given`, `When`, `Then`, `And` or `But` is a step, and a step definition is the code that runs for one step. The [Gherkin reference](https://cucumber.io/docs/gherkin/reference/) covers the full syntax.
25
17
 
18
+ **Upgrading** from version 2, or from `jest-cucumber-fusion` 0.8.x? Your feature and step definition files keep their shape, and step files written as CommonJS keep their `require`. [Migrating to version 3](./docs/Migrating.md) lists what changes.
26
19
 
20
+ **Know Cucumber already?** [If you know Cucumber](#if-you-know-cucumber) lists what differs.
27
21
 
28
22
  ## Getting Started
29
23
 
30
- These steps set up a project whose files are ES modules (they use `import` and `export`), which is the recommended way to use Fusion. If your project is CommonJS (it uses `require`), or you need Node 18, follow the same steps with the changes in [Using CommonJS instead](#using-commonjs-instead).
24
+ These steps set up a project whose files are ES modules (they use `import` and `export`), which is the recommended way to use Fusion: ES modules are the module format Node and Jest are moving to, and Fusion's own source is written in them. If your project is CommonJS (it uses `require`), or you need Node 18 (CommonJS runs from Node 18.14), follow these steps with the changes in [Using CommonJS](./docs/CommonJS.md). For step files written in TypeScript, see [Using TypeScript](./docs/TypeScript.md).
31
25
 
32
- For ES modules you need Node 20.11 or newer.
26
+ ### Start a project
33
27
 
34
- ### Install Jest and Jest Cucumber Fusion:
28
+ Check that `node --version` prints 20.11 or newer (for CommonJS, 18.14: see [Using CommonJS](./docs/CommonJS.md)), then:
35
29
 
36
30
  ```
31
+ mkdir my-project
32
+ cd my-project
33
+ npm init -y
37
34
  npm install --save-dev jest @g_package/jest-cucumber-fusion
35
+ npm pkg set type=module
36
+ npm pkg set scripts.test="node --experimental-vm-modules node_modules/jest/bin/jest.js"
37
+ npm pkg set "jest.testMatch[0]=**/*.steps.js"
38
38
  ```
39
39
 
40
- Coming from `jest-cucumber-fusion` 0.8.x or from version 2 of this package? Your feature and step definition files keep their shape; the import name changes, and several behaviours that used to pass silently now fail with a message saying what to fix. [Migrating to version 3](./docs/Migrating.md) lists each change, observed under both versions.
41
-
42
- ### Set these keys in your package.json:
40
+ The last three commands set these keys in your `package.json`, replacing any that `npm init` wrote:
43
41
 
44
42
  ```json
45
43
  "type": "module",
@@ -47,10 +45,8 @@ Coming from `jest-cucumber-fusion` 0.8.x or from version 2 of this package? Your
47
45
  "jest": { "testMatch": [ "**/*.steps.js" ] }
48
46
  ```
49
47
 
50
- Replace any `type` and `scripts.test` that are already there rather than adding a second copy: `npm init -y` writes `"type": "commonjs"` and a placeholder `test` script, and when a key appears twice in `package.json` the last one silently wins.
51
-
52
- - `"type": "module"` makes Node read your `.js` files as ES modules. If you would rather not set it, name your files `.mjs` instead, and change `testMatch` to `[ "**/*.steps.mjs" ]`.
53
- - Jest runs ES modules only in its ES module mode, which Node's `--experimental-vm-modules` flag turns on. This form of the `test` script is the one Jest's own documentation gives, and it does not depend on your shell's syntax. Node prints an `ExperimentalWarning` about the flag on each run. That is expected.
48
+ - `"type": "module"` makes Node read your `.js` files as ES modules. If you would rather not set it, skip `npm pkg set type=module` and name your step files and the code they import `.mjs` instead (and import them by that name, for example `'../../src/basket.mjs'`), and change `testMatch` to `[ "**/*.steps.mjs" ]`.
49
+ - Jest runs ES modules only in its ES module mode, which Node's `--experimental-vm-modules` flag turns on, so always run the tests with `npm test`, not `npx jest`. Node prints an `ExperimentalWarning` about the flag on each run. That is expected.
54
50
  - `testMatch` tells Jest that your step definition files are the test files.
55
51
 
56
52
  ### Lay out your project
@@ -58,139 +54,83 @@ Replace any `type` and `scripts.test` that are already there rather than adding
58
54
  The examples below use this layout. Any layout works, as long as each `import` path and each `Fusion` path matches where your files are.
59
55
 
60
56
  ```
61
- your-project/
57
+ my-project/
62
58
  ├── package.json
63
59
  ├── src/
64
- │ └── rocket.js the code under test
60
+ │ └── basket.js the code under test
65
61
  └── test/
66
62
  └── features/
67
- ├── rocket-launching.feature
68
- └── rocket-launching.steps.js
63
+ ├── shopping-basket.feature
64
+ └── shopping-basket.steps.js
69
65
  ```
70
66
 
71
67
  The code under test in these examples is a small class of your own:
72
68
 
73
69
  ```javascript
74
- //filename: src/rocket.js
75
- export class Rocket {
70
+ // filename: src/basket.js
71
+ export class Basket {
76
72
  constructor() {
77
- this.isInSpace = false
78
- this.boostersLanded = false
73
+ this.products = []
79
74
  }
80
75
 
81
- launch() {
82
- this.isInSpace = true
83
- this.boostersLanded = true
76
+ add( product ) {
77
+ this.products.push( product )
84
78
  }
85
79
  }
86
80
  ```
87
81
 
88
- ### Add a Feature file:
82
+ ### Write the feature file
89
83
 
90
84
  ```gherkin
91
- ###filename: test/features/rocket-launching.feature
92
- Feature: Rocket Launching
93
-
94
- Scenario: Launching a SpaceX rocket
95
- Given I am Elon Musk attempting to launch a rocket into space
96
- When I launch the rocket
97
- Then the rocket should end up in space
98
- And the booster(s) should land back on the launch pad
99
- And nobody should doubt me ever again
100
- ```
101
-
102
- ### Add a Cucumber step definition file and load Fusion
103
- ```javascript
104
- //filename: test/features/rocket-launching.steps.js
105
- import { Given, When, Then, And, Fusion } from '@g_package/jest-cucumber-fusion'
85
+ # filename: test/features/shopping-basket.feature
86
+ Feature: Shopping basket
106
87
 
88
+ Scenario: Adding a product to an empty basket
89
+ Given my basket is empty
90
+ When I add a coffee mug to my basket
91
+ Then my basket holds 1 product
92
+ And the product in my basket is a coffee mug
107
93
  ```
108
94
 
109
- Import only the keywords your steps use. `But` is also available.
110
-
111
- **A step binds only to a definition registered with its own keyword.** An `And` step needs an `And(...)` definition, and a `Then(...)` definition with the same text does not serve it. That is why the steps below define `And` for the two `And` lines of the feature. When one definition should serve several keywords, chain it: `Then( And( 'text', fn ) )` registers the same definition under both. A step with no definition makes Fusion refuse the file, naming the step and suggesting the code to add.
112
-
113
- ### Load any dependency you need to do your test
114
-
115
- ```javascript
116
- //filename: test/features/rocket-launching.steps.js
117
- import { Given, When, Then, And, Fusion } from '@g_package/jest-cucumber-fusion'
118
-
119
- import { Rocket } from '../../src/rocket.js'
120
- let rocket
121
-
122
- ```
95
+ ### Write the step definitions
123
96
 
124
- A relative import names the file in full, extension included: `'../../src/rocket.js'`, not `'../../src/rocket'`.
97
+ Each step needs a definition registered with **the same keyword the step uses**. The last step of the feature starts with `And`, so its definition, in the file below, is `And(...)`: a `Then(...)` with the same text would not match it, and Fusion would report the step as having no definition. A `But` step likewise needs a `But(...)` definition: [Rule](./docs/Rule.md) shows one.
125
98
 
126
- ### Add steps definitions:
99
+ The step definition file registers a definition for each step, then calls `Fusion` with the feature file's path:
127
100
 
128
101
  ```javascript
129
- //filename: test/features/rocket-launching.steps.js
102
+ // filename: test/features/shopping-basket.steps.js
130
103
  import { Given, When, Then, And, Fusion } from '@g_package/jest-cucumber-fusion'
131
104
 
132
- import { Rocket } from '../../src/rocket.js'
133
- let rocket
105
+ import { Basket } from '../../src/basket.js'
134
106
 
135
- Given( 'I am Elon Musk attempting to launch a rocket into space', () => {
136
- rocket = new Rocket()
137
- } )
107
+ let basket
138
108
 
139
- When( 'I launch the rocket', () => {
140
- rocket.launch()
109
+ Given( 'my basket is empty', () => {
110
+ basket = new Basket()
141
111
  } )
142
112
 
143
- Then( 'the rocket should end up in space', () => {
144
- expect(rocket.isInSpace).toBe(true)
113
+ When( 'I add a coffee mug to my basket', () => {
114
+ basket.add( 'coffee mug' )
145
115
  } )
146
116
 
147
- And( /^the booster\(s\) should land back on the launch pad$/, () => {
148
- expect(rocket.boostersLanded).toBe(true)
117
+ Then( 'my basket holds 1 product', () => {
118
+ expect( basket.products ).toHaveLength( 1 )
149
119
  } )
150
120
 
151
- And( 'nobody should doubt me ever again', () => {
152
- expect('people').not.toBe('haters')
121
+ And( 'the product in my basket is a coffee mug', () => {
122
+ expect( basket.products[ 0 ] ).toBe( 'coffee mug' )
153
123
  } )
154
- ```
155
124
 
156
- `expect` and the other test globals work as usual. The `jest` object (for `jest.fn()` and friends) is not a global in Jest's ES module mode. Add `@jest/globals` to your `devDependencies` and import it:
157
-
158
- ```javascript
159
- import { jest } from '@jest/globals'
125
+ Fusion( 'shopping-basket.feature' )
160
126
  ```
161
127
 
162
- ### Adding the Fusion() call at the end of the Step definition file
163
- You have to match it with your Cucumber Feature definition file. The path is relative to the step definition file, so a feature file beside it is named on its own:
164
- ```javascript
165
- //filename: test/features/rocket-launching.steps.js
166
- import { Given, When, Then, And, Fusion } from '@g_package/jest-cucumber-fusion'
167
-
168
- import { Rocket } from '../../src/rocket.js'
169
- let rocket
170
-
171
- Given( 'I am Elon Musk attempting to launch a rocket into space', () => {
172
- rocket = new Rocket()
173
- } )
174
-
175
- When( 'I launch the rocket', () => {
176
- rocket.launch()
177
- } )
178
-
179
- Then( 'the rocket should end up in space', () => {
180
- expect(rocket.isInSpace).toBe(true)
181
- } )
182
-
183
- And( /^the booster\(s\) should land back on the launch pad$/, () => {
184
- expect(rocket.boostersLanded).toBe(true)
185
- } )
186
-
187
- And( 'nobody should doubt me ever again', () => {
188
- expect('people').not.toBe('haters')
189
- } )
190
-
191
-
192
- Fusion( 'rocket-launching.feature' )
193
- ```
128
+ - **A string matches a step whose text is exactly that string.** To capture a value from a step, such as a number, match it with a regular expression instead: see [Step definition arguments](./docs/StepDefinitionArguments.md).
129
+ - **`Fusion` comes last**, after every definition, and its path is relative to the step definition file, so a feature file beside it is named on its own.
130
+ - **A relative `import` names the file in full**, extension included: `'../../src/basket.js'`, not `'../../src/basket'`.
131
+ - **A step may be `async`.** Fusion waits for each step before running the next. A scenario is one Jest test, so Jest's timeout, 5 seconds by default, covers all of its steps together: raise it with `"testTimeout"` in the `jest` key if your steps are slow.
132
+ - **`expect` and Jest's other test functions work as usual.** The `jest` object is the exception in ES module mode: see [Troubleshooting](#troubleshooting).
133
+ - **Import only the keywords your steps use.** `But` is also available.
194
134
 
195
135
  ### Run the tests
196
136
 
@@ -198,373 +138,136 @@ Fusion( 'rocket-launching.feature' )
198
138
  npm test
199
139
  ```
200
140
 
201
- Jest reports one test for the scenario, named after it, and fails it at the first step that fails.
202
-
203
- Writing your steps in TypeScript? See [Using TypeScript](#using-typescript).
204
-
205
- ## Adding coverage
206
- Fusion's tests are Jest tests, so Jest's coverage works as usual. Set these keys in your `package.json`:
207
- ```json
208
- "jest": {
209
- "testMatch": [
210
- "**/*.steps.js"
211
- ],
212
- "coveragePathIgnorePatterns": [
213
- "/node_modules/",
214
- "/test/"
215
- ],
216
- "coverageDirectory": "./coverage/",
217
- "collectCoverage": true
218
- }
219
- ```
220
-
221
- For TypeScript step files, match `**/*.steps.ts` instead (see [Using TypeScript](#using-typescript)).
222
-
223
-
224
- ## Setting options once for a whole run
141
+ Jest reports one test for the scenario, named after it:
225
142
 
226
- Options can be passed to a single `Fusion` call:
143
+ ```text
144
+ PASS test/features/shopping-basket.steps.js
145
+ Shopping basket
146
+ ✓ Adding a product to an empty basket (2 ms)
227
147
 
228
- ```javascript
229
- Fusion( 'rocket-launching.feature', { tagFilter: '@smoke and not @slow' } )
148
+ Test Suites: 1 passed, 1 total
149
+ Tests: 1 passed, 1 total
230
150
  ```
231
151
 
232
- Or set once for every step definition file of a run, from a script that Jest's `setupFiles` lists. A global tag filter applies to every feature, so a scenario without a matching tag, such as the untagged rocket scenario above, is reported as skipped:
152
+ If Jest prints only the summary, without the list of tests, see [Troubleshooting](#troubleshooting). A test fails at the first step that fails, and names that step. If a step has no definition at all, Fusion refuses the whole file before any test runs, and gives you the code to add. Without the `And` definition above, for example:
233
153
 
234
- ```javascript
235
- //filename: jest-fusion-config.js
236
- import { setFusionConfiguration } from '@g_package/jest-cucumber-fusion'
154
+ ```text
155
+ FAIL test/features/shopping-basket.steps.js
156
+ ● Test suite failed to run
237
157
 
238
- setFusionConfiguration( { tagFilter: '@smoke and not @slow' } )
158
+ Fusion found 1 step in the feature "Shopping basket" that no registered step definition matches.
159
+ ...
160
+ 1. No step definition matches: "the product in my basket is a coffee mug"
161
+ And("the product in my basket is a coffee mug", () => {});
239
162
  ```
240
163
 
241
- ```json
242
- "jest": {
243
- "testMatch": [ "**/*.steps.js" ],
244
- "setupFiles": [ "./jest-fusion-config.js" ]
245
- }
246
- ```
247
-
248
- A per-call option still wins for its own file. See [Configuration options](./docs/AdditionalConfiguration.md) for every option and for the merge order.
249
-
250
- If you are coming from version 2, global configuration used to go through `jest-cucumber`'s own `setJestCucumberConfiguration`. That package is no longer a dependency, so the import moves to `setFusionConfiguration` from this package; the options object is the same shape.
251
-
252
- ## Linting with ESLint
253
-
254
- [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) gives ESLint Jest's globals, such as `expect`, and its rules. One rule needs to be told about Fusion. `jest/no-standalone-expect` reports every `expect` that is not inside a `test` or `it` block, and in a step definition file every `expect` sits inside `Then`, `And` or another step function instead. Each step function and each `Before` and `After` hook runs inside the test that Fusion makes for its scenario, so tell the rule to treat them as test blocks.
164
+ ### Run one scenario
255
165
 
256
- Install ESLint and the plugin:
166
+ While you work on one scenario, you can run it alone. Each scenario is a Jest test named after it, so Jest's `-t` option selects it by name; the other tests are reported as skipped. `-t` takes a regular expression and matches it anywhere in the test's full name, so a short phrase can select several tests:
257
167
 
258
168
  ```
259
- npm install --save-dev eslint @eslint/js globals eslint-plugin-jest
169
+ npm test -- -t "Adding a product"
260
170
  ```
261
171
 
262
- Then add an `eslint.config.js` to your project:
172
+ To pick scenarios by tag instead, pass the tag filter from an environment variable:
263
173
 
264
174
  ```javascript
265
- //filename: eslint.config.js
266
- import js from '@eslint/js'
267
- import globals from 'globals'
268
- import jest from 'eslint-plugin-jest'
269
-
270
- export default [
271
- js.configs.recommended,
272
- {
273
- languageOptions: { sourceType: 'module', globals: globals.node },
274
- },
275
- {
276
- files: [ '**/*.steps.js' ],
277
- ...jest.configs[ 'flat/recommended' ],
278
- rules: {
279
- ...jest.configs[ 'flat/recommended' ].rules,
280
- // Fusion runs each step and hook inside the test it makes for the scenario.
281
- 'jest/no-standalone-expect': [ 'error', {
282
- additionalTestBlockFunctions: [ 'Given', 'When', 'Then', 'And', 'But', 'Before', 'After' ],
283
- } ],
284
- },
285
- },
286
- ]
175
+ Fusion( 'shopping-basket.feature', { tagFilter: process.env.TAGS } )
287
176
  ```
288
177
 
289
- Run it with `npx eslint .`. Without the `jest/no-standalone-expect` options, every `expect` in the steps file above is reported as `Expect must be inside of a test block`. With them, an `expect` that really is outside a step, a hook or a test is still reported.
178
+ Then tag the scenario, on the line above it:
290
179
 
291
- If your shared step files are not named `*.steps.js` (see [Re-using step definitions](./docs/ReusingStepDefinitions.md)), add their names to `files`.
292
-
293
- ## Using CommonJS instead
294
-
295
- Fusion ships a CommonJS build beside its ES modules: `require` gets the CommonJS build and `import` gets the ES modules. Use CommonJS if your project already is CommonJS, if you need Node 18 (CommonJS runs on Node 18.14 and newer), or if you would rather not run Jest's experimental ES module mode. CommonJS needs no Jest set-up at all.
296
-
297
- The steps in [Getting Started](#getting-started) work with these changes.
298
-
299
- **package.json.** Leave out `"type": "module"`, and run Jest directly:
300
-
301
- ```json
302
- "scripts": { "test": "jest" },
303
- "jest": { "testMatch": [ "**/*.steps.js" ] }
180
+ ```gherkin
181
+ @wip
182
+ Scenario: Adding a product to an empty basket
304
183
  ```
305
184
 
306
- **The code under test** exports with `module.exports`:
307
-
308
- ```javascript
309
- //filename: src/rocket.js
310
- class Rocket {
311
- constructor() {
312
- this.isInSpace = false
313
- this.boostersLanded = false
314
- }
185
+ and run `TAGS=@wip npm test`. That form of setting a variable works in macOS and Linux shells; in Windows PowerShell, set `$env:TAGS = '@wip'` first, then run `npm test`. With `TAGS` unset, this call adds no filter of its own, so every scenario runs unless a [global configuration](./docs/AdditionalConfiguration.md#global-configuration) sets one. [Tag filtering](./docs/AdditionalConfiguration.md#tag-filtering) covers the expressions you can use, such as `@wip and not @slow`.
315
186
 
316
- launch() {
317
- this.isInSpace = true
318
- this.boostersLanded = true
319
- }
320
- }
187
+ ## Hooks
321
188
 
322
- module.exports = { Rocket }
323
- ```
324
-
325
- **Step definition files** `require` the same names. A relative `require` may leave the extension out:
189
+ `Before` and `After` run a function before or after every scenario of the `Fusion` call that follows them, which is the place to give each scenario a fresh start. With a `Before` hook, the steps above no longer need to create the basket:
326
190
 
327
191
  ```javascript
328
- //filename: test/features/rocket-launching.steps.js
329
- const { Given, When, Then, And, Fusion } = require( '@g_package/jest-cucumber-fusion' )
192
+ import { Before, Given, When, Then, And, Fusion } from '@g_package/jest-cucumber-fusion'
330
193
 
331
- const { Rocket } = require( '../../src/rocket' )
332
- let rocket
194
+ import { Basket } from '../../src/basket.js'
333
195
 
334
- Given( 'I am Elon Musk attempting to launch a rocket into space', () => {
335
- rocket = new Rocket()
336
- } )
196
+ let basket
337
197
 
338
- When( 'I launch the rocket', () => {
339
- rocket.launch()
198
+ Before( () => {
199
+ basket = new Basket()
340
200
  } )
341
201
 
342
- Then( 'the rocket should end up in space', () => {
343
- expect(rocket.isInSpace).toBe(true)
202
+ Given( 'my basket is empty', () => {
203
+ expect( basket.products ).toHaveLength( 0 )
344
204
  } )
345
205
 
346
- And( /^the booster\(s\) should land back on the launch pad$/, () => {
347
- expect(rocket.boostersLanded).toBe(true)
206
+ When( 'I add a coffee mug to my basket', () => {
207
+ basket.add( 'coffee mug' )
348
208
  } )
349
209
 
350
- And( 'nobody should doubt me ever again', () => {
351
- expect('people').not.toBe('haters')
210
+ Then( 'my basket holds 1 product', () => {
211
+ expect( basket.products ).toHaveLength( 1 )
352
212
  } )
353
213
 
214
+ And( 'the product in my basket is a coffee mug', () => {
215
+ expect( basket.products[ 0 ] ).toBe( 'coffee mug' )
216
+ } )
354
217
 
355
- Fusion( 'rocket-launching.feature' )
356
- ```
357
-
358
- The `jest` object is a global, as in any CommonJS Jest project.
359
-
360
- **A global configuration script** `require`s `setFusionConfiguration`:
361
-
362
- ```javascript
363
- //filename: jest-fusion-config.js
364
- const { setFusionConfiguration } = require( '@g_package/jest-cucumber-fusion' )
365
-
366
- setFusionConfiguration( { tagFilter: '@smoke and not @slow' } )
367
- ```
368
-
369
- **ESLint.** The configuration file is CommonJS too, with `sourceType: 'commonjs'`:
370
-
371
- ```javascript
372
- //filename: eslint.config.js
373
- const js = require( '@eslint/js' )
374
- const globals = require( 'globals' )
375
- const jest = require( 'eslint-plugin-jest' )
376
-
377
- module.exports = [
378
- js.configs.recommended,
379
- {
380
- languageOptions: { sourceType: 'commonjs', globals: globals.node },
381
- },
382
- {
383
- files: [ '**/*.steps.js' ],
384
- ...jest.configs[ 'flat/recommended' ],
385
- rules: {
386
- ...jest.configs[ 'flat/recommended' ].rules,
387
- // Fusion runs each step and hook inside the test it makes for the scenario.
388
- 'jest/no-standalone-expect': [ 'error', {
389
- additionalTestBlockFunctions: [ 'Given', 'When', 'Then', 'And', 'But', 'Before', 'After' ],
390
- } ],
391
- },
392
- },
393
- ]
394
- ```
395
-
396
- **Mixing the two styles** works. A CommonJS shared step library can serve ES module step files, and a CommonJS setup script can configure ES module steps: both styles share one set of step definitions and one global configuration. An ES module imports a CommonJS file by its full name, for example `import './shared-steps.cjs'`.
397
-
398
- The other pages in this documentation show ES modules. To use one of their examples in CommonJS, turn each `import { … } from '…'` into `const { … } = require( '…' )` and each `export` into `module.exports`.
399
-
400
- ## Using TypeScript
401
-
402
- Fusion ships its type definitions, and TypeScript finds the right ones for ES modules and CommonJS with no configuration. To run step definition files written in TypeScript, Jest needs a transform. [ts-jest](https://kulshekhar.github.io/ts-jest/) is the usual one.
403
-
404
- ```
405
- npm install --save-dev jest ts-jest typescript @types/jest @g_package/jest-cucumber-fusion
406
- ```
407
-
408
- ### ES modules (recommended)
409
-
410
- ```json
411
- "type": "module",
412
- "scripts": {
413
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
414
- "typecheck": "tsc --noEmit"
415
- }
416
- ```
417
-
418
- ```json
419
- //filename: tsconfig.json
420
- {
421
- "compilerOptions": {
422
- "target": "ES2022",
423
- "module": "NodeNext",
424
- "moduleResolution": "NodeNext",
425
- "isolatedModules": true,
426
- "strict": true,
427
- "types": [ "jest" ]
428
- }
429
- }
430
- ```
431
-
432
- ```javascript
433
- //filename: jest.config.js
434
- export default {
435
- preset: 'ts-jest/presets/default-esm',
436
- testMatch: [ '**/*.steps.ts' ],
437
- moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
438
- }
439
- ```
440
-
441
- **Run `npm run typecheck` as well as `npm test`, for example in CI.** In ES module mode ts-jest only strips the types and does not check them, so a type error does not fail `npm test`; `tsc --noEmit` is what reports it. (`isolatedModules` tells ts-jest that this is expected. Without it, ts-jest warns `TS151002` and still does not check.)
442
-
443
- `moduleNameMapper` lets your imports keep the `.js` extension that ES modules need, while Jest loads the `.ts` file: `import { Rocket } from '../../src/rocket.js'` loads `src/rocket.ts`.
444
-
445
- ### CommonJS
446
-
447
- Leave out `"type": "module"`, compile to CommonJS, and use ts-jest's default preset:
448
-
449
- ```json
450
- "scripts": {
451
- "test": "jest",
452
- "typecheck": "tsc --noEmit"
453
- }
454
- ```
455
-
456
- ```json
457
- {
458
- "compilerOptions": {
459
- "target": "ES2022",
460
- "module": "CommonJS",
461
- "strict": true,
462
- "types": [ "jest" ]
463
- }
464
- }
465
- ```
466
-
467
- `"module": "CommonJS"` is what lets ts-jest check types here. With `NodeNext` or `Node16`, ts-jest warns `TS151002` and asks for `isolatedModules`, which would switch its checking off.
468
-
469
- ```javascript
470
- //filename: jest.config.js
471
- module.exports = {
472
- preset: 'ts-jest',
473
- testMatch: [ '**/*.steps.ts' ],
474
- moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
475
- }
476
- ```
477
-
478
- Here ts-jest checks types while it runs the tests, so a type error fails `npm test` too.
479
-
480
- ### Typed step definitions
481
-
482
- To move the [Getting Started](#getting-started) example to TypeScript, renaming its files to `.ts` is not enough under `strict`: `tsc --noEmit` then reports the `Rocket` class's fields as undeclared (`TS2339`) and `let rocket` as an implicit `any` (`TS7034`). Declare the fields in the class and write `let rocket: Rocket`, as the example below does. A step may also declare the type of each argument it receives: a capture or a docstring arrives as a `string`, and a data table as an `Array<Record<string, string>>` of its rows. For example, with this feature:
483
-
484
- ```gherkin
485
- ###filename: test/features/launch.feature
486
- Feature: Launch
487
-
488
- Scenario: Launching a batch of rockets
489
- Given I am launching 3 rockets
490
- When the countdown says
491
- """
492
- ignition
493
- """
494
- Then the manifest has
495
- | name |
496
- | Falcon |
497
- ```
498
-
499
- and this code under test:
500
-
501
- ```typescript
502
- //filename: src/launch.ts
503
- export class Launch {
504
- launched = 0
505
- count: number
506
-
507
- constructor( count: number ) {
508
- this.count = count
509
- }
510
-
511
- countdown( word: string ): void {
512
- if ( word === 'ignition' ) this.launched = this.count
513
- }
514
- }
218
+ Fusion( 'shopping-basket.feature' )
515
219
  ```
516
220
 
517
- the steps declare a `string` for the capture and the docstring, and the rows for the table:
221
+ Register hooks, like step definitions, before the `Fusion` call. A hook may be `async`, and it receives nothing, so declare no parameters: Jest treats a declared parameter as a `done` callback and waits for it to be called.
518
222
 
519
- ```typescript
520
- //filename: test/features/launch.steps.ts
521
- import { Given, When, Then, Fusion } from '@g_package/jest-cucumber-fusion'
223
+ ## If you know Cucumber
522
224
 
523
- import { Launch } from '../../src/launch.js'
225
+ Most of Gherkin works as you expect: [`Background`](./docs/Background.md), [`Rule`](./docs/Rule.md), `Scenario Outline` with `Examples`, data tables, docstrings, tags and [80 languages](./docs/Language.md). The differences:
524
226
 
525
- let launch: Launch
227
+ - **Steps bind by keyword.** An `And` step needs an `And` definition, as above. When one definition should serve several keywords, chain it: `Then( And( 'text', fn ) )` registers it under both.
228
+ - **No Cucumber Expressions.** A string is compared as plain text, so `'I add {int} coffee mugs to my basket'` matches only that exact text. Use a regular expression to capture values: `/^I add (\d+) coffee mugs to my basket$/`.
229
+ - **No `*` steps.** Because a step binds by its keyword, Fusion refuses a `*` step and asks you to write it with `Given`, `When`, `Then`, `And` or `But`.
230
+ - **No World.** Steps are plain functions with no shared `this`. Share state through variables in the step definition file, and reset it in a `Before` hook.
231
+ - **Hooks take no tags, and there is no `BeforeAll` or `AfterAll`.** `Before` and `After` run around every scenario of their `Fusion` call. For once-per-file set-up, use Jest's own `beforeAll` and `afterAll` at the top of the step definition file.
232
+ - **Tags select scenarios through an option**, `tagFilter`, rather than a command-line flag: see [Configuration options](./docs/AdditionalConfiguration.md#tag-filtering). Jest's own `-t` filter also works, on scenario names.
526
233
 
527
- Given( /^I am launching (\d+) rockets$/, ( count: string ) => {
528
- launch = new Launch( Number( count ) )
529
- } )
234
+ ## Where next
530
235
 
531
- When( 'the countdown says', ( words: string ) => {
532
- launch.countdown( words.trim() )
533
- } )
236
+ The [documentation index](./docs/README.md) suggests an order to read the guides in. In short:
534
237
 
535
- Then( 'the manifest has', ( rows: Array<Record<string, string>> ) => {
536
- expect( rows ).toStrictEqual( [ { name: 'Falcon' } ] )
537
- expect( launch.launched ).toBe( 3 )
538
- } )
238
+ | To | Read |
239
+ |---|---|
240
+ | capture values from a step, or read a docstring | [Step definition arguments](./docs/StepDefinitionArguments.md) |
241
+ | give every scenario the same first steps | [Background](./docs/Background.md) |
242
+ | use a data table | [Gherkin tables](./docs/GherkinTables.md) |
243
+ | run one scenario for every row of an `Examples` table | [Scenario outlines](./docs/ScenarioOutlines.md) |
244
+ | group scenarios under a business rule | [Rule](./docs/Rule.md) |
245
+ | share steps between feature files | [Re-using step definitions](./docs/ReusingStepDefinitions.md) |
246
+ | filter by tag, name tests, or set options once for every file | [Configuration options](./docs/AdditionalConfiguration.md) |
247
+ | use CommonJS, or Node 18 | [Using CommonJS](./docs/CommonJS.md) |
248
+ | write step definitions in TypeScript | [Using TypeScript](./docs/TypeScript.md) |
249
+ | lint step files with ESLint | [Linting with ESLint](./docs/ESLint.md) |
250
+ | write feature files in another language | [Language](./docs/Language.md) |
251
+ | upgrade from version 2 or 0.8.x | [Migrating to version 3](./docs/Migrating.md) |
539
252
 
540
- Fusion( 'launch.feature' )
541
- ```
253
+ ## Coverage
542
254
 
543
- An argument you leave undeclared has the type `StepArgument` (`string | Array<Record<string, string>>`), which you can import from the package.
255
+ Coverage works as in any Jest project, because Fusion's tests are Jest tests: run `npm test -- --coverage`. Jest already leaves the `*.steps.js` files out of the report; to leave out shared step files under `test/` as well, add `"coveragePathIgnorePatterns": [ "/node_modules/", "/test/" ]` to the `jest` key.
544
256
 
545
257
  ## Troubleshooting
546
258
 
547
- - **`SyntaxError: Unexpected token 'with'`** when Jest loads your ES module steps: your Node is older than 20.11, which ES module steps need. Upgrade Node, or [use CommonJS](#using-commonjs-instead), which runs from Node 18.14.
548
- - **`npm warn EBADENGINE` on Node 18**, naming `brace-expansion` or `lru-cache`: these come from Jest 30's own dependencies, not from Fusion. CommonJS steps still run on Node 18.14 and newer.
549
- - **ESLint on Node 18:** ESLint 10 itself needs Node 20.19 or newer. On Node 18.18 or newer, install ESLint 9 and the matching Jest plugin instead (`npm install --save-dev eslint@9 @eslint/js@9 eslint-plugin-jest@28 globals`); the configuration above works with them.
550
- - **`No step definition matches`** for a step you did define: the step's keyword decides which definitions it can bind to. An `And` step needs an `And(...)` definition (see [Getting Started](#add-a-cucumber-step-definition-file-and-load-fusion)).
259
+ - **`Must use import to load ES Module`**: Jest ran without `--experimental-vm-modules`, so run the tests with `npm test`, not `npx jest`. If you did use `npm test`, a `.cjs` file `require`s an ES module, which Node before 24.9 cannot do (see [Mixing the two styles](./docs/CommonJS.md#mixing-the-two-styles)).
260
+ - **`Cannot use import statement outside a module`**: `package.json` has no `"type": "module"`. Set it (see [Start a project](#start-a-project)), or name your files `.mjs`. On Jest 29 and older, Jest run without `--experimental-vm-modules` gives this message too: run the tests with `npm test`.
261
+ - **`SyntaxError: Unexpected token 'with'`** when Jest loads your ES module steps: your Node cannot parse the import attributes in Fusion's ES modules (Node 18.19 and 20.9 fail; 20.11 is the oldest measured to work). Upgrade Node, or [use CommonJS](./docs/CommonJS.md), which runs from Node 18.14.
262
+ - **`jest is not defined`**: in Jest's ES module mode the `jest` object (for `jest.fn()` and friends) is not a global. Add `@jest/globals` to your `devDependencies` and write `import { jest } from '@jest/globals'`.
263
+ - **`No step definition matches`** for a step you did define: the step's keyword decides which definitions it can bind to. An `And` step needs an `And(...)` definition (see [Write the step definitions](#write-the-step-definitions)).
264
+ - **`Your test suite must contain at least one test`**: the feature file is empty, or has a `Feature:` but no scenarios. Add a `Scenario`.
265
+ - **`Error parsing feature Gherkin`**, with `expected: ... #FeatureLine`: the file has scenarios but no `Feature:` line. Add one before the first scenario.
266
+ - **`Feature file not found`**: the path passed to `Fusion` is wrong. It is relative to the step definition file.
267
+ - **`Exceeded timeout of 5000 ms for a test`**: the scenario's steps together took longer than Jest's timeout. Raise `"testTimeout"` in the `jest` key.
268
+ - **A `Before` or `After` hook times out waiting for `done()`**: the hook declares a parameter. Remove it (see [Hooks](#hooks)).
269
+ - **`Multiple configurations found`**: Jest found both a `jest` key in `package.json` and a `jest.config.js`. Keep one: see [Using TypeScript](./docs/TypeScript.md).
270
+ - **`npm WARN EBADENGINE` on Node 18**, naming `brace-expansion` or `lru-cache`: these come from Jest 30's own dependencies, not from Fusion. CommonJS steps still run on Node 18.14 and newer.
551
271
  - **Jest prints only the summary, without the list of tests:** Jest shortens its output when it detects an AI agent's environment variables, such as `AI_AGENT` or `CLAUDECODE`. Run it from a terminal that does not have them.
552
272
 
553
- ## Additional Documentation
554
-
555
- Using the package:
556
-
557
- * [Step definition arguments](./docs/StepDefinitionArguments.md)
558
- * [Gherkin tables](./docs/GherkinTables.md)
559
- * [Scenario outlines](./docs/ScenarioOutlines.md)
560
- * [Re-using step definitions](./docs/ReusingStepDefinitions.md)
561
- * [Language](./docs/Language.md)
562
- * [Configuration options](./docs/AdditionalConfiguration.md): every export and option
563
- * [Migrating to version 3](./docs/Migrating.md)
564
-
565
- Working on the package:
566
-
567
- * [Running the examples](./docs/RunningTheExamples.md)
568
- * [Architecture](./docs/Architecture.md)
569
-
570
- Every example on the guide pages above runs as written: the repository's tests copy each one into a fresh project and run it.
273
+ Still stuck, or found a bug? [Open an issue](https://github.com/gotreasa/jest-cucumber-fusion/issues/new/choose).
package/dist/index.d.cts CHANGED
@@ -92,18 +92,21 @@ export interface FusionOptions {
92
92
  * they are compared, so the operators may be written in any case too.
93
93
  *
94
94
  * A scenario is selected on the tags its compiled pickle carries, which is the union of the
95
- * scenario's own tags, its feature's tags and, for a Scenario Outline row, that Examples
96
- * set's tags. A scenario the expression excludes is registered through `test.skip` under its
97
- * own unannotated name, so Jest reports it as skipped: never run, and never absent from the
98
- * report. Its steps are also exempt from the unmatched-step check, because a scenario you
95
+ * scenario's own tags, its `Rule`'s tags, its feature's tags and, for a Scenario Outline row,
96
+ * that Examples set's tags. A scenario the expression excludes is registered through `test.skip`
97
+ * under its own unannotated name, so Jest reports it as skipped: never run, and never absent from
98
+ * the report. Its steps are also exempt from the unmatched-step check, because a scenario you
99
99
  * excluded is not one you are asking to have wired. They are still read, so a step with an
100
100
  * unsupported keyword (`*`) or one that matches two definitions is refused all the same.
101
101
  *
102
102
  * An expression that cannot be parsed is refused at collection, before anything is
103
103
  * registered, and the refusal names the expression you wrote. So is one with an operand
104
104
  * that is not a tag, such as `smoke` written for `@smoke`: it could never match.
105
+ *
106
+ * `null` means no filter. Unlike `undefined`, which counts as not set, it is a value, so
107
+ * `{ tagFilter: null }` on one call clears a filter set by `setFusionConfiguration`.
105
108
  */
106
- tagFilter?: string;
109
+ tagFilter?: string | null;
107
110
  /**
108
111
  * Renames every test Fusion registers, Scenario Outline rows included. It is called once per
109
112
  * test while Fusion registers, never again while the tests run, and it names a test whatever
@@ -136,8 +139,9 @@ export function Fusion(feature: string, options?: FusionOptions): void;
136
139
  * as `{ tagFilter: process.env.TAGS }`, counts as not set and leaves the layer below in place.
137
140
  *
138
141
  * A second call REPLACES what the first set rather than merging into it, which is what lets a
139
- * global be cleared or redefined. Jest gives each test file its own module registry and runs
140
- * `setupFiles` inside it, so what is set here cannot reach another file.
142
+ * global be cleared or redefined. Jest gives each test file its own global object and runs
143
+ * `setupFiles` inside it. The configuration lives on that global, not in a module, so what is
144
+ * set here cannot reach another file, and `jest.resetModules()` does not clear it.
141
145
  *
142
146
  * An argument that is not an options object is refused where it is called, before any step
143
147
  * definition file has loaded. An unknown key is accepted and ignored, exactly as it is per call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@g_package/jest-cucumber-fusion",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Write cucumber test as part of a jest run (including coverage)",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "pretest": "npm run build",
18
18
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --color",
19
- "test-d": "tsd",
19
+ "test-d": "tstyche",
20
20
  "test:baseline": "node test/specs/baseline/run-all.js",
21
21
  "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
22
22
  "lint": "eslint . --max-warnings 0",
@@ -51,6 +51,7 @@
51
51
  "@eslint/js": "^10.0.1",
52
52
  "@secretlint/secretlint-rule-pattern": "^13.0.7",
53
53
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.7",
54
+ "@semantic-release/exec": "^7.1.0",
54
55
  "@types/jest": "^30.0.0",
55
56
  "@types/node": "^18.19.130",
56
57
  "czg": "1.14.0",
@@ -62,16 +63,19 @@
62
63
  "husky": "^9.1.7",
63
64
  "jest": "^30.4.2",
64
65
  "lint-staged": "^17.6.0",
65
- "prettier": "3.9.9",
66
+ "prettier": "3.9.10",
66
67
  "secretlint": "^13.0.7",
67
68
  "semantic-release": "^25.0.5",
68
- "tsd": "^0.33.0",
69
+ "tstyche": "^7.2.5",
69
70
  "typescript": "^6.0.3"
70
71
  },
71
72
  "publishConfig": {
72
73
  "access": "public"
73
74
  },
74
75
  "release": {
76
+ "branches": [
77
+ "main"
78
+ ],
75
79
  "plugins": [
76
80
  "@semantic-release/commit-analyzer",
77
81
  "@semantic-release/release-notes-generator",
@@ -79,7 +83,13 @@
79
83
  "@semantic-release/github",
80
84
  {
81
85
  "successComment": false,
82
- "failComment": false
86
+ "failComment": false,
87
+ "assets": [
88
+ {
89
+ "path": "release/*.cdx.json",
90
+ "label": "SBOM (CycloneDX)"
91
+ }
92
+ ]
83
93
  }
84
94
  ],
85
95
  [
@@ -88,6 +98,12 @@
88
98
  "npmPublish": false,
89
99
  "tarballDir": "release"
90
100
  }
101
+ ],
102
+ [
103
+ "@semantic-release/exec",
104
+ {
105
+ "prepareCmd": "npm sbom --sbom-format cyclonedx --omit dev --package-lock-only > release/jest-cucumber-fusion-${nextRelease.version}.cdx.json"
106
+ }
91
107
  ]
92
108
  ],
93
109
  "parserOpts": {
package/src/index.d.ts CHANGED
@@ -92,18 +92,21 @@ export interface FusionOptions {
92
92
  * they are compared, so the operators may be written in any case too.
93
93
  *
94
94
  * A scenario is selected on the tags its compiled pickle carries, which is the union of the
95
- * scenario's own tags, its feature's tags and, for a Scenario Outline row, that Examples
96
- * set's tags. A scenario the expression excludes is registered through `test.skip` under its
97
- * own unannotated name, so Jest reports it as skipped: never run, and never absent from the
98
- * report. Its steps are also exempt from the unmatched-step check, because a scenario you
95
+ * scenario's own tags, its `Rule`'s tags, its feature's tags and, for a Scenario Outline row,
96
+ * that Examples set's tags. A scenario the expression excludes is registered through `test.skip`
97
+ * under its own unannotated name, so Jest reports it as skipped: never run, and never absent from
98
+ * the report. Its steps are also exempt from the unmatched-step check, because a scenario you
99
99
  * excluded is not one you are asking to have wired. They are still read, so a step with an
100
100
  * unsupported keyword (`*`) or one that matches two definitions is refused all the same.
101
101
  *
102
102
  * An expression that cannot be parsed is refused at collection, before anything is
103
103
  * registered, and the refusal names the expression you wrote. So is one with an operand
104
104
  * that is not a tag, such as `smoke` written for `@smoke`: it could never match.
105
+ *
106
+ * `null` means no filter. Unlike `undefined`, which counts as not set, it is a value, so
107
+ * `{ tagFilter: null }` on one call clears a filter set by `setFusionConfiguration`.
105
108
  */
106
- tagFilter?: string;
109
+ tagFilter?: string | null;
107
110
  /**
108
111
  * Renames every test Fusion registers, Scenario Outline rows included. It is called once per
109
112
  * test while Fusion registers, never again while the tests run, and it names a test whatever
@@ -136,8 +139,9 @@ export function Fusion(feature: string, options?: FusionOptions): void;
136
139
  * as `{ tagFilter: process.env.TAGS }`, counts as not set and leaves the layer below in place.
137
140
  *
138
141
  * A second call REPLACES what the first set rather than merging into it, which is what lets a
139
- * global be cleared or redefined. Jest gives each test file its own module registry and runs
140
- * `setupFiles` inside it, so what is set here cannot reach another file.
142
+ * global be cleared or redefined. Jest gives each test file its own global object and runs
143
+ * `setupFiles` inside it. The configuration lives on that global, not in a module, so what is
144
+ * set here cannot reach another file, and `jest.resetModules()` does not clear it.
141
145
  *
142
146
  * An argument that is not an options object is refused where it is called, before any step
143
147
  * definition file has loaded. An unknown key is accepted and ignored, exactly as it is per call.