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

support: re-add setDefinitionFunctionWrapper (minus generator step logic) #1795

Merged
merged 16 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO
----
## [Unreleased] (In Git)

See the [migration guide](./docs/migration.md) for details of how to migrate from 7.x.x to 8.x.x

### Breaking changes

* Drop support for Node.js 10 and 15, add support for Node.js 16
* Remove deprecated `--retryTagFilter` option (the correct option is `--retry-tag-filter`)
* Remove `setDefinitionFunctionWrapper` and step definition option `wrapperOptions`
* Drop native support for generator functions used as step definitions
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
* Remove `--predictable-ids` option (was only used for internal testing)

### Added
Expand Down
1 change: 1 addition & 0 deletions dependency-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ignoreErrors:
- '@typescript-eslint/eslint-plugin' # peer dependency of standard-with-typescript
- '@typescript-eslint/parser' # peer dependency of @typescript-eslint/eslint-plugin
- '@types/*' # type definitions
- bluebird # features/generator_step_definitions.feature
- coffeescript # features/compiler.feature
- eslint-config-prettier # .eslintrc.yml - extends - prettier
- eslint-config-standard-with-typescript # .eslintrc.yml - extends - standard-with-typescript
Expand Down
31 changes: 25 additions & 6 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
# Migrating from 7.x.x to 8.x.x
# Migrating to cucumber-js 8.x.x

## Removal of setDefinitionFunctionWrapper
## Generator step definitions

If this was used to wrap generator functions, please transition to using async / await.
If this was used to wrap step definitions, please use `BeforeStep` / `AfterStep` hooks instead.
If you had other use cases, please create an issue.
Generator functions used in step definitions (`function*` with the `yield` keyword)
are not natively supported anymore with cucumber-js.

# Migrating from 6.x.x to 7.x.x
You may consider using `async`/`await` rather than generators.

You can still use generators as before but you need to add your own dependencies
to `bluebird` and `is-generator`. Cucumber-js will no display explicit error message
anymore in case you use a generator without wrapping it properly.

```javascript
const isGenerator = require('is-generator')
const {coroutine} = require('bluebird')
const {setDefinitionFunctionWrapper} = require('@cucumber/cucumber')

setDefinitionFunctionWrapper(function (fn) {
if (isGenerator.fn(fn)) {
return coroutine(fn)
} else {
return fn
}
})
```

# Migrating to cucumber-js 7.x.x

## Package Name

Expand Down
32 changes: 32 additions & 0 deletions docs/support_files/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Aliases: `Given`, `When`, `Then`.
* `pattern`: A regex or string pattern to match against a gherkin step.
* `options`: An object with the following keys:
- `timeout`: A step-specific timeout, to override the default timeout.
- `wrapperOptions`: Step-specific options that are passed to the definition function wrapper.
* `fn`: A function, which should be defined as follows:
- Should have one argument for each capture in the regular expression.
- May have an additional argument if the gherkin step has a docstring or data table.
Expand All @@ -132,6 +133,37 @@ Set the default timeout for asynchronous steps. Defaults to `5000` milliseconds.

---

#### `setDefinitionFunctionWrapper(wrapper)`
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved

_Note: the usage of `setDefinitionFunctionWrapper` is discouraged in favor of [BeforeStep](#beforestepoptions-fn) and [AfterStep](#afterstepoptions-fn) hooks._

Set a function used to wrap step / hook definitions.

The `wrapper` function is expected to take 2 arguments:

- `fn` is the original function defined for the step - needs to be called in order for the step to be run.
- `options` is the step specific `wrapperOptions` and may be undefined.

Example:

```javascript
setDefinitionFunctionWrapper(function(fn, options) {
return function(...args) {
// call original function with correct `this` and arguments
// ensure return value of function is returned
return fn.apply(this, args)
.catch(error => {
// rethrow error to avoid swallowing failure
throw error;
});
}
})
```

When used, the result is wrapped again to ensure it has the same length of the original step / hook definition.

---

#### `setWorldConstructor(constructor)`

Set a custom world constructor, to override the default world constructor:
Expand Down
30 changes: 30 additions & 0 deletions docs/support_files/step_definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ When(/^I view my profile$/, function () {
});
```


## Definition function wrapper

If you would like to wrap step or hook definitions in with some additional logic you can use `setDefinitionFunctionWrapper(fn)`. The definitions will be wrapped after they have all been loaded but before the tests begin to run. One example usage is wrapping generator functions to return promises. Cucumber will do an additional stage of wrapping to ensure the function retains its original length.

```javascript
// features/step_definitions/file_steps.js
const { Then } = require('@cucumber/cucumber');
const assert = require('assert');
const mzFs = require('mz/fs');

Then(/^the file named (.*) is empty$/, function *(fileName) {
contents = yield mzFs.readFile(fileName, 'utf8');
assert.equal(contents, '');
});

// features/support/setup.js
const { setDefinitionFunctionWrapper } = require('@cucumber/cucumber');
const isGenerator = require('is-generator');
const Promise = require('bluebird');

setDefinitionFunctionWrapper(function (fn) {
if (isGenerator.fn(fn)) {
return Promise.coroutine(fn);
} else {
return fn;
}
});
```

## Pending steps

Each interface has its own way of marking a step as pending
Expand Down
35 changes: 35 additions & 0 deletions features/step_wrapper_with_options.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Feature: Step Wrapper with Options
In order to be able to write more complex step definition wrappers
As a developer
I want Cucumber to provide the "options" object to the wrapping function

@spawn
Scenario: options passed to the step definitions wrapper
Given a file named "features/a.feature" with:
"""
Feature: Step with an option
Scenario: Steps
When I run a step with options
"""
And a file named "features/step_definitions/cucumber_steps.js" with:
"""
const {When} = require('@cucumber/cucumber')

When(/^I run a step with options$/, {wrapperOptions: {retry: 2}}, function () {})
"""
And a file named "features/support/setup.js" with:
"""
const {setDefinitionFunctionWrapper} = require('@cucumber/cucumber')

setDefinitionFunctionWrapper(function (fn, options = {}) {
if (options.retry) {
console.log("Max retries: ", options.retry);
}
return fn;
})
"""
When I run cucumber-js
Then the output contains the text:
"""
Max retries: 2
"""
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
"stacktrace-js": "^2.0.2",
"string-argv": "^0.3.1",
"tmp": "^0.2.1",
"util-arity": "^1.1.0",
"verror": "^1.10.0"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions src/cli/helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ describe('helpers', () => {
stepDefinitions: [
new StepDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '0',
line: 9,
options: {},
Expand Down Expand Up @@ -172,6 +173,7 @@ describe('helpers', () => {
stepDefinitions: [
new StepDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '0',
line: 9,
options: {},
Expand Down Expand Up @@ -209,6 +211,7 @@ describe('helpers', () => {
beforeTestCaseHookDefinitions: [
new TestCaseHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '0',
line: 3,
options: {
Expand All @@ -220,13 +223,15 @@ describe('helpers', () => {
afterTestCaseHookDefinitions: [
new TestCaseHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '1',
line: 7,
options: {},
uri: 'features/support/hooks.js',
}),
new TestCaseHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '2',
line: 11,
options: {},
Expand Down Expand Up @@ -280,6 +285,7 @@ describe('helpers', () => {
beforeTestRunHookDefinitions: [
new TestRunHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '0',
line: 3,
options: {},
Expand All @@ -289,13 +295,15 @@ describe('helpers', () => {
afterTestRunHookDefinitions: [
new TestRunHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '1',
line: 7,
options: {},
uri: 'features/support/run-hooks.js',
}),
new TestRunHookDefinition({
code: noopFunction,
unwrappedCode: noopFunction,
id: '2',
line: 11,
options: {},
Expand Down
2 changes: 1 addition & 1 deletion src/formatter/helpers/usage_helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function buildEmptyMapping(
const mapping: Record<string, IUsage> = {}
stepDefinitions.forEach((stepDefinition) => {
mapping[stepDefinition.id] = {
code: stepDefinition.code.toString(),
code: stepDefinition.unwrappedCode.toString(),
line: stepDefinition.line,
pattern: stepDefinition.expression.source,
patternType: stepDefinition.expression.constructor.name,
Expand Down
75 changes: 56 additions & 19 deletions src/formatter/helpers/usage_helpers/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,65 @@ import { buildSupportCodeLibrary } from '../../../../test/runtime_helpers'
describe('Usage Helpers', () => {
describe('getUsage', () => {
describe('with step definitions', () => {
it('includes stringified code', async () => {
// Arrange
const code = function (): string {
return 'original code'
}
const supportCodeLibrary = buildSupportCodeLibrary(({ Given }) => {
Given('a step', code)
})
const { eventDataCollector } = await getEnvelopesAndEventDataCollector({
supportCodeLibrary,
})
describe('without function definition wrapper', () => {
it('includes stringified code', async () => {
// Arrange
const code = function (): string {
return 'original code'
}
const supportCodeLibrary = buildSupportCodeLibrary(({ Given }) => {
Given('a step', code)
})
const { eventDataCollector } =
await getEnvelopesAndEventDataCollector({ supportCodeLibrary })

// Act
const output = getUsage({
cwd: '/project',
eventDataCollector,
stepDefinitions: supportCodeLibrary.stepDefinitions,
// Act
const output = getUsage({
cwd: '/project',
eventDataCollector,
stepDefinitions: supportCodeLibrary.stepDefinitions,
})

// Assert
expect(output).to.have.lengthOf(1)
expect(output[0].code).to.eql(code.toString())
})
})

// Assert
expect(output).to.have.lengthOf(1)
expect(output[0].code).to.eql(code.toString())
describe('with function definition wrapper', () => {
it('includes unwrapped version of stringified code', async () => {
// Arrange
const code = function (): string {
return 'original code'
}
const supportCodeLibrary = buildSupportCodeLibrary(
({ Given, setDefinitionFunctionWrapper }) => {
Given('a step', code)
setDefinitionFunctionWrapper(
(fn: Function) =>
function (fn: Function) {
if (fn.length === 1) {
return fn
}
return fn
}
)
}
)
const { eventDataCollector } =
await getEnvelopesAndEventDataCollector({ supportCodeLibrary })

// Act
const output = getUsage({
cwd: '/project',
eventDataCollector,
stepDefinitions: supportCodeLibrary.stepDefinitions,
})

// Assert
expect(output).to.have.lengthOf(1)
expect(output[0].code).to.eql(code.toString())
})
})
})
})
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const defineParameterType = methods.defineParameterType
export const defineStep = methods.defineStep
export const Given = methods.Given
export const setDefaultTimeout = methods.setDefaultTimeout
export const setDefinitionFunctionWrapper = methods.setDefinitionFunctionWrapper
export const setWorldConstructor = methods.setWorldConstructor
export const Then = methods.Then
export const When = methods.When
Expand Down
Loading