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: add ability to name hooks #1994

Merged
merged 16 commits into from Apr 20, 2022
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber.

## [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))

### 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))

Expand Down
4 changes: 4 additions & 0 deletions compatibility/features/hooks/hooks.ts
Expand Up @@ -6,6 +6,10 @@ Before(function () {
// no-op
})

Before({ name: 'A named hook' }, function () {
// no-op
})

When('a step passes', function () {
// no-op
})
Expand Down
24 changes: 20 additions & 4 deletions docs/support_files/hooks.md
Expand Up @@ -6,7 +6,7 @@ Note that your hook functions cannot reference the [world](./world.md) as `this`
arrow functions. See [FAQ](../faq.md) for details.

```javascript
var {After, Before} = require('@cucumber/cucumber');
const {After, Before} = require('@cucumber/cucumber');

// Synchronous
Before(function () {
Expand All @@ -33,12 +33,28 @@ After(function () {
});
```

## Named hooks

ℹ️ Added in v8.1.0

Hooks can optionally be named:

```javascript
const {Before} = require('@cucumber/cucumber');

Before({name: "Set up some test state"}, function () {
// do stuff here
});
```

Such hooks will then be referenced by name in [formatter](../formatters.md) output, which can be useful to help you understand what's happening with your tests.

## Tagged hooks

Hooks can be conditionally selected for execution based on the tags of the scenario.

```javascript
var {After, Before} = require('@cucumber/cucumber');
const {After, Before} = require('@cucumber/cucumber');

Before(function () {
// This hook will be executed before all scenarios
Expand Down Expand Up @@ -85,7 +101,7 @@ If you have some setup / teardown that needs to be done before or after all scen
Unlike `Before` / `After` these methods will not have a world instance as `this`. This is because each scenario gets its own world instance and these hooks run before / after **all** scenarios.

```javascript
var {AfterAll, BeforeAll} = require('@cucumber/cucumber');
const {AfterAll, BeforeAll} = require('@cucumber/cucumber');

// Synchronous
BeforeAll(function () {
Expand All @@ -111,7 +127,7 @@ AfterAll(function () {
If you have some code execution that needs to be done before or after all steps, use `BeforeStep` / `AfterStep`. Like the `Before` / `After` hooks, these also have a world instance as 'this', and can be conditionally selected for execution based on the tags of the scenario.

```javascript
var {AfterStep, BeforeStep} = require('@cucumber/cucumber');
const {AfterStep, BeforeStep} = require('@cucumber/cucumber');

BeforeStep({tags: "@foo"}, function () {
// This hook will be executed before all steps in a scenario with tag @foo
Expand Down
35 changes: 35 additions & 0 deletions features/named_hooks.feature
@@ -0,0 +1,35 @@
Feature: Named hooks

As a developer
I want to name a `Before` or `After` hook
So that I can easily identify which hooks are run when reporting

Scenario: Hook is named and then referenced by its name in formatter output
Given a file named "features/a.feature" with:
"""
Feature: some feature
Scenario: some scenario
Given a step
"""
And a file named "features/step_definitions/hooks.js" with:
"""
const {After, Before} = require('@cucumber/cucumber')

Before({name: 'hook 1'}, function() {})
Before({name: 'hook 2'}, function() {})
After({name: 'hook 3'}, function() {})
"""
And a file named "features/step_definitions/steps.js" with:
"""
const {Given} = require('@cucumber/cucumber')

Given('a step', function() {
throw 'nope'
})
"""
When I run cucumber-js
Then it fails
And the output contains the text:
"""
Before (hook 2) #
"""
40 changes: 23 additions & 17 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -200,7 +200,7 @@
"@cucumber/gherkin": "23.0.1",
"@cucumber/gherkin-streams": "5.0.1",
"@cucumber/gherkin-utils": "^7.0.0",
"@cucumber/html-formatter": "19.0.0",
"@cucumber/html-formatter": "19.1.0",
"@cucumber/messages": "18.0.0",
"@cucumber/tag-expressions": "4.1.0",
"assertion-error-formatter": "^3.0.0",
Expand Down Expand Up @@ -230,7 +230,7 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@cucumber/compatibility-kit": "9.1.2",
"@cucumber/compatibility-kit": "9.2.0",
"@cucumber/message-streams": "4.0.1",
"@cucumber/query": "11.0.0",
"@microsoft/api-documenter": "7.17.0",
Expand Down
1 change: 1 addition & 0 deletions src/cli/helpers.ts
Expand Up @@ -195,6 +195,7 @@ function emitTestCaseHooks(
const envelope: messages.Envelope = {
hook: {
id: testCaseHookDefinition.id,
name: testCaseHookDefinition.name,
tagExpression: testCaseHookDefinition.tagExpression,
sourceReference: {
uri: testCaseHookDefinition.uri,
Expand Down
8 changes: 7 additions & 1 deletion src/cli/helpers_spec.ts
Expand Up @@ -220,6 +220,7 @@ describe('helpers', () => {
id: '0',
line: 3,
options: {
name: 'before hook',
tags: '@hooks-tho',
},
uri: 'features/support/hooks.js',
Expand All @@ -231,7 +232,9 @@ describe('helpers', () => {
unwrappedCode: noopFunction,
id: '1',
line: 7,
options: {},
options: {
name: 'after hook',
},
uri: 'features/support/hooks.js',
}),
new TestCaseHookDefinition({
Expand All @@ -249,6 +252,7 @@ describe('helpers', () => {
{
hook: {
id: '0',
name: 'before hook',
tagExpression: '@hooks-tho',
sourceReference: {
uri: 'features/support/hooks.js',
Expand All @@ -261,6 +265,7 @@ describe('helpers', () => {
{
hook: {
id: '1',
name: 'after hook',
tagExpression: undefined,
sourceReference: {
uri: 'features/support/hooks.js',
Expand All @@ -273,6 +278,7 @@ describe('helpers', () => {
{
hook: {
id: '2',
name: undefined,
tagExpression: undefined,
sourceReference: {
uri: 'features/support/hooks.js',
Expand Down
4 changes: 4 additions & 0 deletions src/formatter/helpers/test_case_attempt_formatter.ts
Expand Up @@ -49,13 +49,17 @@ function formatStep({
printAttachments,
}: IFormatStepRequest): string {
const {
name,
result: { status },
actionLocation,
attachments,
} = testStep
const colorFn = colorFns.forStatus(status)
const identifier = testStep.keyword + valueOrDefault(testStep.text, '')
let text = colorFn(`${CHARACTERS.get(status)} ${identifier}`)
if (doesHaveValue(name)) {
text += colorFn(` (${name})`)
}
if (doesHaveValue(actionLocation)) {
text += ` # ${colorFns.location(formatLocation(actionLocation))}`
}
Expand Down
2 changes: 2 additions & 0 deletions src/formatter/helpers/test_case_attempt_parser.ts
Expand Up @@ -18,6 +18,7 @@ export interface IParsedTestStep {
argument?: messages.PickleStepArgument
attachments: messages.Attachment[]
keyword: string
name?: string
result: messages.TestStepResult
snippet?: string
sourceLocation?: ILineAndUri
Expand Down Expand Up @@ -87,6 +88,7 @@ function parseStep({
uri: hookDefinition.uri,
line: hookDefinition.line,
}
out.name = hookDefinition.name
}
if (
doesHaveValue(testStep.stepDefinitionIds) &&
Expand Down
1 change: 1 addition & 0 deletions src/models/definition.ts
Expand Up @@ -20,6 +20,7 @@ export interface IDefinitionOptions {
}

export interface IHookDefinitionOptions extends IDefinitionOptions {
name?: string
tags?: string
}

Expand Down
2 changes: 2 additions & 0 deletions src/models/test_case_hook_definition.ts
Expand Up @@ -12,11 +12,13 @@ export default class TestCaseHookDefinition
extends Definition
implements IDefinition
{
public readonly name: string
public readonly tagExpression: string
private readonly pickleTagFilter: PickleTagFilter

constructor(data: IDefinitionParameters<IHookDefinitionOptions>) {
super(data)
this.name = data.options.name
this.tagExpression = data.options.tags
this.pickleTagFilter = new PickleTagFilter(data.options.tags)
}
Expand Down
1 change: 1 addition & 0 deletions src/support_code_library_builder/types.ts
Expand Up @@ -48,6 +48,7 @@ export interface IDefineStepOptions {
}

export interface IDefineTestCaseHookOptions {
name?: string
tags?: string
timeout?: number
}
Expand Down
4 changes: 4 additions & 0 deletions test-d/hooks.ts
Expand Up @@ -23,6 +23,10 @@ After(function (param: ITestCaseHookParameter) {})
BeforeStep(function (param: ITestStepHookParameter) {})
AfterStep(function (param: ITestStepHookParameter) {})

// should allow an object with tags and/or name in hooks
Before({ tags: '@foo', name: 'before hook' }, function () {})
After({ tags: '@foo', name: 'after hook' }, function () {})

// should allow us to return 'skipped' from a test case hook
Before(async function () {
return 'skipped'
Expand Down