From 98da7331f3506aa04ceaf2b23e425ee2a9f58cc6 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sun, 17 Apr 2022 12:07:41 +0100 Subject: [PATCH 1/5] add generics for world params --- src/support_code_library_builder/world.ts | 17 ++++++++----- test-d/world.ts | 31 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/support_code_library_builder/world.ts b/src/support_code_library_builder/world.ts index 1d142c4ac..1406c7c38 100644 --- a/src/support_code_library_builder/world.ts +++ b/src/support_code_library_builder/world.ts @@ -1,24 +1,27 @@ import { ICreateAttachment, ICreateLog } from '../runtime/attachment_manager' -export interface IWorldOptions { +export interface IWorldOptions { attach: ICreateAttachment log: ICreateLog - parameters: any + parameters: ParametersType } -export interface IWorld { +export interface IWorld { 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 + implements IWorld +{ 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) { this.attach = attach this.log = log this.parameters = parameters diff --git a/test-d/world.ts b/test-d/world.ts index 45bc75cb6..7cf652cf1 100644 --- a/test-d/world.ts +++ b/test-d/world.ts @@ -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 @@ -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) { + this.log(this.parameters.foo) +}) +expectError( + Before(async function (this: IWorld) { + this.log(this.parameters.bar) + }) +) + +// should allow us to use a custom parameters type with a custom world +class CustomWorldWithParameters extends World { + 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) + }) +) From f340b6cd54d397ee251ee3d9f8914c00d24d2198 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sun, 17 Apr 2022 12:16:44 +0100 Subject: [PATCH 2/5] some tweaks to existing world doc --- docs/support_files/world.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/support_files/world.md b/docs/support_files/world.md index b536f961d..e1deb7431 100644 --- a/docs/support_files/world.md +++ b/docs/support_files/world.md @@ -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') @@ -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 @@ -41,13 +43,13 @@ 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: * `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. @@ -66,10 +68,12 @@ This option is repeatable, so you can use it multiple times and the objects will 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"; @@ -117,8 +121,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" From a87961ac885239b2ec3ecdbc62dbf841d2f0f280 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sun, 17 Apr 2022 12:56:44 +0100 Subject: [PATCH 3/5] document ctor as fn --- docs/support_files/world.md | 41 +++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/support_files/world.md b/docs/support_files/world.md index e1deb7431..f1398bcfa 100644 --- a/docs/support_files/world.md +++ b/docs/support_files/world.md @@ -45,7 +45,7 @@ Then("my color should not be blue", () => { ## 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 @@ -66,7 +66,44 @@ This option is repeatable, so you can use it multiple times and the objects will ## 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 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. +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) +}) +``` ### Real-world example From f6eca8f834e26c170c3445ed5bf13fe06d4a566b Mon Sep 17 00:00:00 2001 From: David Goss Date: Sun, 17 Apr 2022 13:14:36 +0100 Subject: [PATCH 4/5] update world.md documentation --- docs/support_files/world.md | 50 +++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/support_files/world.md b/docs/support_files/world.md index f1398bcfa..7f4ed80cb 100644 --- a/docs/support_files/world.md +++ b/docs/support_files/world.md @@ -53,7 +53,7 @@ By default, the world is an instance of Cucumber's built-in `World` class. Cucum 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. @@ -64,7 +64,7 @@ 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: @@ -183,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 { + // 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. From 9c8de6fbaf8346c3be41085962d34bd930c6f1d7 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 20 Apr 2022 09:14:50 +0100 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10016b7cf..7a024da65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO ## [Unreleased] ### Added - Add support for named hooks (see [documentation](./docs/support_files/hooks.md#named-hooks)) ([#1994](https://github.com/cucumber/cucumber-js/pull/1994)) +- 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)) ### Changed - 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))