Skip to content

Commit e60f503

Browse files
world: extend docs around classes and functions, support generics for parameters (#2002)
* add generics for world params * some tweaks to existing world doc * document ctor as fn * update world.md documentation * Update CHANGELOG.md Co-authored-by: Aurélien Reeves <[email protected]>
1 parent c45330a commit e60f503

File tree

4 files changed

+139
-19
lines changed

4 files changed

+139
-19
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO
1010
## [Unreleased]
1111
### Added
1212
- Add support for named hooks (see [documentation](./docs/support_files/hooks.md#named-hooks)) ([#1994](https://github.com/cucumber/cucumber-js/pull/1994))
13+
- Add generics support for world parameters type in world-related interfaces and classes (see [documentation](./docs/support_files/world.md#typescript)) ([#1968](https://github.com/cucumber/cucumber-js/issues/1968) [#2002](https://github.com/cucumber/cucumber-js/pull/2002))
1314

1415
### Changed
1516
- Rename the `cucumber-js` binary's underlying file to be `cucumber.js`, so it doesn't fall foul of Node.js module conventions and plays nicely with ESM loaders (see [documentation](./docs/esm.md#transpiling)) ([#1993](https://github.com/cucumber/cucumber-js/pull/1993))

docs/support_files/world.md

+98-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# World
22

3-
*World*, or sometimes *context*, is an isolated scope for each scenario, exposed to the steps and most hooks as `this`. It allows you to set variables in one step and recall them in a later step. All variables set this way are discarded when the scenario concludes. It is managed by a world class, either the default one or one you create. Each scenario is given an new instance of the class when the test starts, even if it is a [retry run](../retry.md).
3+
*World*, is an isolated scope for each scenario, exposed to the steps and most hooks as `this`. It allows you to set variables in one step and recall them in a later step. All variables set this way are discarded when the scenario concludes. It is managed by a world class, either the default one or one you create. Each scenario is given an new instance of the class when the test starts, even if it is a [retry run](../retry.md).
44

55
The world is not available to the hooks `BeforeAll` or `AfterAll` as each of these executes outside any particular scenario.
66

7-
##### Basic Example
7+
Here's some simple usage of the world to retain state between steps:
8+
89
```javascript
910
const { Given, Then } = require('@cucumber/cucumber')
1011

@@ -18,7 +19,8 @@ Then("my color should not be red", function() {
1819
}
1920
});
2021
```
21-
With those step definitions in place
22+
23+
With those step definitions in place:
2224

2325
```gherkin
2426
Scenario: Will pass
@@ -41,17 +43,17 @@ Then("my color should not be blue", () => {
4143
});
4244
```
4345

44-
## Cucumber World
46+
## Built-in world
4547

46-
Cucumber provides a number of formatting helpers that are passed into the constructor of the World. The default world binds these helpers as follows:
48+
By default, the world is an instance of Cucumber's built-in `World` class. Cucumber provides a number of formatting helpers that are passed into the constructor as an options object. The default world binds these helpers as follows:
4749

4850
* `this.attach`: a method for adding [attachments](./attachments.md) to hooks/steps
4951
* `this.log`: a method for [logging](./attachments.md#logging) information from hooks/steps
50-
* `this.parameters`: an object of parameters passed in via the [CLI](../cli.md#world-parameters)
52+
* `this.parameters`: an object of parameters passed in via configuration (see below)
5153

5254
Your custom world will also receive these arguments, but it's up to you to decide what to do with them and they can be safely ignored.
5355

54-
### World Parameters
56+
### World parameters
5557

5658
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.
5759

@@ -62,14 +64,53 @@ The `worldParameters` configuration option allows you to provide this informatio
6264

6365
This option is repeatable, so you can use it multiple times and the objects will be merged with the later ones taking precedence.
6466

65-
## Custom Worlds
67+
## Custom worlds
68+
69+
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 write your own world implementation with its own properties and methods that help with your instrumentation, and then call `setWorldConstructor` to tell Cucumber about it:
70+
71+
```javascript
72+
const { setWorldConstructor, World, When } = require('@cucumber/cucumber')
73+
74+
class CustomWorld extends World {
75+
count = 0
76+
77+
constructor(options) {
78+
super(options)
79+
}
80+
81+
eat(count) {
82+
this.count += count
83+
}
84+
}
85+
86+
setWorldConstructor(CustomWorld)
87+
88+
When('I eat {int} cucumbers', function(count) {
89+
this.eat(count)
90+
})
91+
```
92+
93+
In the example above we've extended the built-in `World` class, which is recommended. You can also use a plain function as your world constructor:
94+
95+
```javascript
96+
const { setWorldConstructor, When } = require('@cucumber/cucumber')
97+
98+
setWorldConstructor(function(options) {
99+
this.count = 0
100+
this.eat = (count) => this.count += count
101+
})
102+
103+
When('I eat {int} cucumbers', function(count) {
104+
this.eat(count)
105+
})
106+
```
66107

67-
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.
108+
### Real-world example
68109

69110
Let's walk through a typical scenario, setting up world that manages a browser context. We'll use the ES6 module syntax for this example. First, let's set up our custom world. Class files should not be loaded as steps - they should be imported. So in this example we'll presume it is in a classes folder next to the steps folder.
70111

71-
###### CustomWorld.js
72112
```javascript
113+
// CustomWorld.js
73114
import { World } from '@cucumber/cucumber';
74115
import seleniumWebdriver from "selenium-webdriver";
75116

@@ -117,8 +158,8 @@ export default class extends World {
117158

118159
Now we'll use a step file to setup this custom world and declare the before hook.
119160

120-
###### setup.js
121161
```javascript
162+
// setup.js
122163
import { Before, setWorldConstructor } from '@cucumber/cucumber';
123164
import CustomWorld from "../classes/CustomWorld.js"
124165

@@ -142,6 +183,52 @@ Given("I'm viewing the admin settings", async function(){
142183

143184
This pattern allows for cleaner feature files. Remember that, ideally, scenarios should be between 3-5 lines and communicate **what** the user is doing clearly to the whole team without going into the details of **how** it will be done. While steps can be reused that should not come at the expense of feature clarity.
144185

186+
## TypeScript
187+
188+
If you're using TypeScript, you can get optimum type safety and completion based on your custom world and parameters.
189+
190+
### Hooks and steps
191+
192+
If you have a custom world, you'll need to tell TypeScript about the type of `this` in your hook and step functions:
193+
194+
```typescript
195+
When('I eat {int} cucumbers', function(this: CustomWorld, count: number) {
196+
this.eat(count)
197+
})
198+
```
199+
200+
### World parameters
201+
202+
ℹ️ Added in v8.1.0
203+
204+
If you're using world parameters (see above), Cucumber's world-related interfaces and classes support generics to easily specify their interface:
205+
206+
```typescript
207+
interface CustomParameters {
208+
cukeLimit: number
209+
}
210+
211+
class CustomWorld extends World<CustomParameters> {
212+
// etc
213+
}
214+
```
215+
216+
### Plain functions
217+
218+
If you're using a plain function as your world constructor, you'll need to define an interface for your world and type that as `this` for your function:
219+
220+
```typescript
221+
interface CustomWorld {
222+
count: number
223+
eat: (count: number) => void
224+
}
225+
226+
setWorldConstructor(function(this: CustomWorld, options: IWorldOptions) {
227+
this.count = 0
228+
this.eat = (count) => this.count += count
229+
})
230+
```
231+
145232
## Summary
146233
- The *World* provides an isolated context for your tests.
147234
- It allows you to track test state while maintaining the isolation of each scenario.

src/support_code_library_builder/world.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import { ICreateAttachment, ICreateLog } from '../runtime/attachment_manager'
22

3-
export interface IWorldOptions {
3+
export interface IWorldOptions<ParametersType = any> {
44
attach: ICreateAttachment
55
log: ICreateLog
6-
parameters: any
6+
parameters: ParametersType
77
}
88

9-
export interface IWorld {
9+
export interface IWorld<ParametersType = any> {
1010
readonly attach: ICreateAttachment
1111
readonly log: ICreateLog
12-
readonly parameters: any
12+
readonly parameters: ParametersType
13+
1314
[key: string]: any
1415
}
1516

16-
export default class World implements IWorld {
17+
export default class World<ParametersType = any>
18+
implements IWorld<ParametersType>
19+
{
1720
public readonly attach: ICreateAttachment
1821
public readonly log: ICreateLog
19-
public readonly parameters: any
22+
public readonly parameters: ParametersType
2023

21-
constructor({ attach, log, parameters }: IWorldOptions) {
24+
constructor({ attach, log, parameters }: IWorldOptions<ParametersType>) {
2225
this.attach = attach
2326
this.log = log
2427
this.parameters = parameters

test-d/world.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Before, setWorldConstructor, When, World } from '../'
1+
import { Before, setWorldConstructor, When, IWorld, World } from '../'
22
import { expectError } from 'tsd'
33

44
// should allow us to read parameters and add attachments
@@ -44,3 +44,32 @@ Before(async function (this: CustomWorld) {
4444
When('stuff happens', async function (this: CustomWorld) {
4545
this.doThing()
4646
})
47+
48+
// should allow us to use a custom parameters type without a custom world
49+
interface CustomParameters {
50+
foo: string
51+
}
52+
Before(async function (this: IWorld<CustomParameters>) {
53+
this.log(this.parameters.foo)
54+
})
55+
expectError(
56+
Before(async function (this: IWorld<CustomParameters>) {
57+
this.log(this.parameters.bar)
58+
})
59+
)
60+
61+
// should allow us to use a custom parameters type with a custom world
62+
class CustomWorldWithParameters extends World<CustomParameters> {
63+
doThing(): string {
64+
return 'foo'
65+
}
66+
}
67+
setWorldConstructor(CustomWorldWithParameters)
68+
Before(async function (this: CustomWorldWithParameters) {
69+
this.log(this.parameters.foo)
70+
})
71+
expectError(
72+
Before(async function (this: CustomWorldWithParameters) {
73+
this.log(this.parameters.bar)
74+
})
75+
)

0 commit comments

Comments
 (0)