Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

world: extend docs around classes and functions, support generics for parameters #2002

Merged
merged 7 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 98 additions & 11 deletions docs/support_files/world.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# World

*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).
*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).

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

##### Basic Example
Here's some simple usage of the world to retain state between steps:

```javascript
const { Given, Then } = require('@cucumber/cucumber')

Expand All @@ -18,7 +19,8 @@ Then("my color should not be red", function() {
}
});
```
With those step definitions in place

With those step definitions in place:

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

## Cucumber World
## Built-in world

Cucumber provides a number of formatting helpers that are passed into the constructor of the World. The default world binds these helpers as follows:
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:

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

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.

### World Parameters
### World parameters

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.

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

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

## Custom Worlds
## 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 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:

```javascript
const { setWorldConstructor, World, When } = require('@cucumber/cucumber')

class CustomWorld extends World {
count = 0

constructor(options) {
super(options)
}

eat(count) {
this.count += count
}
}

setWorldConstructor(CustomWorld)

When('I eat {int} cucumbers', function(count) {
this.eat(count)
})
```

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:

```javascript
const { setWorldConstructor, When } = require('@cucumber/cucumber')

setWorldConstructor(function(options) {
this.count = 0
this.eat = (count) => this.count += count
})

When('I eat {int} cucumbers', function(count) {
this.eat(count)
})
```

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.
### Real-world example

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.

###### CustomWorld.js
```javascript
// CustomWorld.js
import { World } from '@cucumber/cucumber';
import seleniumWebdriver from "selenium-webdriver";

Expand Down Expand Up @@ -117,8 +158,8 @@ export default class extends World {

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

###### setup.js
```javascript
// setup.js
import { Before, setWorldConstructor } from '@cucumber/cucumber';
import CustomWorld from "../classes/CustomWorld.js"

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

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.

## TypeScript

If you're using TypeScript, you can get optimum type safety and completion based on your custom world and parameters.

### Hooks and steps

If you have a custom world, you'll need to tell TypeScript about the type of `this` in your hook and step functions:

```typescript
When('I eat {int} cucumbers', function(this: CustomWorld, count: number) {
this.eat(count)
})
```

### World parameters

ℹ️ Added in v8.1.0

If you're using world parameters (see above), Cucumber's world-related interfaces and classes support generics to easily specify their interface:

```typescript
interface CustomParameters {
cukeLimit: number
}

class CustomWorld extends World<CustomParameters> {
// etc
}
```

### Plain functions

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:

```typescript
interface CustomWorld {
count: number
eat: (count: number) => void
}

setWorldConstructor(function(this: CustomWorld, options: IWorldOptions) {
this.count = 0
this.eat = (count) => this.count += count
})
```

## Summary
- The *World* provides an isolated context for your tests.
- It allows you to track test state while maintaining the isolation of each scenario.
Expand Down
17 changes: 10 additions & 7 deletions src/support_code_library_builder/world.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { ICreateAttachment, ICreateLog } from '../runtime/attachment_manager'

export interface IWorldOptions {
export interface IWorldOptions<ParametersType = any> {
attach: ICreateAttachment
log: ICreateLog
parameters: any
parameters: ParametersType
}

export interface IWorld {
export interface IWorld<ParametersType = any> {
readonly attach: ICreateAttachment
readonly log: ICreateLog
readonly parameters: any
readonly parameters: ParametersType

[key: string]: any
}

export default class World implements IWorld {
export default class World<ParametersType = any>
implements IWorld<ParametersType>
{
public readonly attach: ICreateAttachment
public readonly log: ICreateLog
public readonly parameters: any
public readonly parameters: ParametersType

constructor({ attach, log, parameters }: IWorldOptions) {
constructor({ attach, log, parameters }: IWorldOptions<ParametersType>) {
this.attach = attach
this.log = log
this.parameters = parameters
Expand Down
31 changes: 30 additions & 1 deletion test-d/world.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Before, setWorldConstructor, When, World } from '../'
import { Before, setWorldConstructor, When, IWorld, World } from '../'
import { expectError } from 'tsd'

// should allow us to read parameters and add attachments
Expand Down Expand Up @@ -44,3 +44,32 @@ Before(async function (this: CustomWorld) {
When('stuff happens', async function (this: CustomWorld) {
this.doThing()
})

// should allow us to use a custom parameters type without a custom world
interface CustomParameters {
foo: string
}
Before(async function (this: IWorld<CustomParameters>) {
this.log(this.parameters.foo)
})
expectError(
Before(async function (this: IWorld<CustomParameters>) {
this.log(this.parameters.bar)
})
)

// should allow us to use a custom parameters type with a custom world
class CustomWorldWithParameters extends World<CustomParameters> {
doThing(): string {
return 'foo'
}
}
setWorldConstructor(CustomWorldWithParameters)
Before(async function (this: CustomWorldWithParameters) {
this.log(this.parameters.foo)
})
expectError(
Before(async function (this: CustomWorldWithParameters) {
this.log(this.parameters.bar)
})
)