Skip to content

Commit

Permalink
support: add ability to name hooks (#1994)
Browse files Browse the repository at this point in the history
* update compatibility kit

* add failing feature test

* add failing type test

* add name to object

* enough to make cck pass

* update formatter stuff so feature test passes

* fix this test

* add doco

* more feature file text

* Update CHANGELOG.md

* Update CHANGELOG.md

* tweak documentation

* update html-formatter dep

Co-authored-by: Aurélien Reeves <aurelien.reeves@smartbear.com>
  • Loading branch information
davidjgoss and aurelien-reeves committed Apr 20, 2022
1 parent dfb3f79 commit c45330a
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 24 deletions.
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

0 comments on commit c45330a

Please sign in to comment.