Skip to content

Commit 4e6a9ee

Browse files
authored
add BeforeStep and AfterStep hooks (#1416)
1 parent b90fc80 commit 4e6a9ee

15 files changed

+777
-7
lines changed

docs/support_files/api_reference.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ Defines a hook which is run after each scenario.
3636
* `tags`: String tag expression used to apply this hook to only specific scenarios. See [cucumber-tag-expressions](https://docs.cucumber.io/tag-expressions/) for more information.
3737
* `timeout`: A hook-specific timeout, to override the default timeout.
3838
* `fn`: A function, defined as follows:
39-
* The first argument will be an object of the form `{sourceLocation: {line, uri}, result: {duration, status, exception?}, pickle}`
40-
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin-v4.1.3/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
39+
* The first argument will be an object of the form `{pickle, gherkinDocument, result, testCaseStartedId}`
40+
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin/v15.0.2/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
4141
* When using the asynchronous callback interface, have one final argument for the callback function.
4242

4343
`options` can also be a string as a shorthand for specifying `tags`.
@@ -59,6 +59,24 @@ Multiple `AfterAll` hooks are executed in the **reverse** order that they are de
5959

6060
---
6161

62+
#### `AfterStep([options,] fn)`
63+
64+
Defines a hook which is run after each step.
65+
66+
* `options`: An object with the following keys:
67+
* `tags`: String tag expression used to apply this hook to only specific scenarios. See [cucumber-tag-expressions](https://docs.cucumber.io/tag-expressions/) for more information.
68+
* `timeout`: A hook-specific timeout, to override the default timeout.
69+
* `fn`: A function, defined as follows:
70+
* The first argument will be an object of the form `{pickle, gherkinDocument, result, testCaseStartedId, testStepId}`
71+
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin/v15.0.2/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
72+
* When using the asynchronous callback interface, have one final argument for the callback function.
73+
74+
`options` can also be a string as a shorthand for specifying `tags`.
75+
76+
Multiple `AfterStep` hooks are executed in the **reverse** order that they are defined.
77+
78+
---
79+
6280
#### `Before([options,] fn)`
6381

6482
Defines a hook which is run before each scenario. Same interface as `After` except the first argument passed to `fn` will not have the `result` property.
@@ -75,6 +93,14 @@ Multiple `BeforeAll` hooks are executed in the order that they are defined.
7593

7694
---
7795

96+
#### `BeforeStep([options,] fn)`
97+
98+
Defines a hook which is run before each step. Same interface as `AfterStep` except the first argument passed to `fn` will not have the `result` property.
99+
100+
Multiple `BeforeStep` hooks are executed in the order that they are defined.
101+
102+
---
103+
78104
#### `defineStep(pattern[, options], fn)`
79105

80106
Defines a step.

docs/support_files/hooks.md

+20
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,23 @@ AfterAll(function () {
102102
return Promise.resolve()
103103
});
104104
```
105+
106+
## BeforeStep / AfterStep
107+
108+
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.
109+
110+
```javascript
111+
var {AfterStep, BeforeStep} = require('cucumber');
112+
113+
BeforeStep({tags: "@foo"}, function () {
114+
// This hook will be executed before all steps in a scenario with tag @foo
115+
});
116+
117+
AfterStep( function ({result}) {
118+
// This hook will be executed after all steps, and take a screenshot on step failure
119+
if (result.status === Status.FAILED) {
120+
this.driver.takeScreenshot();
121+
}
122+
});
123+
```
124+
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
Feature: Before and After Step Hooks
2+
3+
Background:
4+
Given a file named "features/a.feature" with:
5+
"""
6+
Feature: some feature
7+
@this-tag
8+
Scenario: some scenario
9+
Given a step
10+
"""
11+
And a file named "features/step_definitions/cucumber_steps.js" with:
12+
"""
13+
const {Given} = require('@cucumber/cucumber')
14+
Given(/^a step$/, function() {})
15+
"""
16+
17+
Scenario: Before and After Hooks work correctly
18+
Given a file named "features/support/hooks.js" with:
19+
"""
20+
const {BeforeStep, AfterStep, BeforeAll, AfterAll} = require('@cucumber/cucumber')
21+
const {expect} = require('chai')
22+
23+
let counter = 1
24+
25+
BeforeStep(function() {
26+
counter = counter + 1
27+
})
28+
29+
AfterStep(function() {
30+
expect(counter).to.eql(2)
31+
counter = counter + 1
32+
})
33+
34+
AfterAll(function() {
35+
expect(counter).to.eql(3)
36+
})
37+
"""
38+
When I run cucumber-js
39+
Then it passes
40+
41+
Scenario: Failing before step fails the scenario
42+
Given a file named "features/support/hooks.js" with:
43+
"""
44+
const {BeforeStep} = require('@cucumber/cucumber')
45+
BeforeStep(function() { throw 'Fail' })
46+
"""
47+
When I run cucumber-js
48+
Then it fails
49+
50+
Scenario: Failing after step fails the scenario
51+
Given a file named "features/support/hooks.js" with:
52+
"""
53+
const {AfterStep} = require('@cucumber/cucumber')
54+
AfterStep(function() { throw 'Fail' })
55+
"""
56+
When I run cucumber-js
57+
Then it fails
58+
59+
Scenario: Only run BeforeStep hooks with appropriate tags
60+
Given a file named "features/support/hooks.js" with:
61+
"""
62+
const { BeforeStep } = require('@cucumber/cucumber')
63+
BeforeStep({tags: "@any-tag"}, function() {
64+
throw Error("Would fail if ran")
65+
})
66+
"""
67+
When I run cucumber-js
68+
Then it passes
69+
70+
Scenario: Only run BeforeStep hooks with appropriate tags
71+
Given a file named "features/support/hooks.js" with:
72+
"""
73+
const { AfterStep } = require('@cucumber/cucumber')
74+
AfterStep({tags: "@this-tag"}, function() {
75+
throw Error("Would fail if ran")
76+
})
77+
"""
78+
When I run cucumber-js
79+
Then it fails
80+
81+
Scenario: after hook parameter can access result status of step
82+
Given a file named "features/support/hooks.js" with:
83+
"""
84+
const { AfterStep, Status } = require('@cucumber/cucumber')
85+
86+
AfterStep(function({result}) {
87+
if (result.status === Status.PASSED) {
88+
return
89+
} else {
90+
throw Error("Result object did not get passed properly to AfterStep Hook.")
91+
}
92+
})
93+
"""
94+
When I run cucumber-js
95+
Then it passes

features/world_in_hooks.feature

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Feature: World in Hooks
2+
3+
Background:
4+
Given a file named "features/a.feature" with:
5+
"""
6+
Feature: some feature
7+
Scenario: some scenario
8+
Given a step
9+
"""
10+
And a file named "features/step_definitions/cucumber_steps.js" with:
11+
"""
12+
const {Given} = require('@cucumber/cucumber')
13+
Given(/^a step$/, function() {})
14+
"""
15+
And a file named "features/support/world.js" with:
16+
"""
17+
const {setWorldConstructor} = require('@cucumber/cucumber')
18+
function WorldConstructor() {
19+
return {
20+
isWorld: function() { return true }
21+
}
22+
}
23+
setWorldConstructor(WorldConstructor)
24+
"""
25+
26+
Scenario: World is this in hooks
27+
Given a file named "features/support/hooks.js" with:
28+
"""
29+
const {After, Before } = require('@cucumber/cucumber')
30+
Before(function() {
31+
if (!this.isWorld()) {
32+
throw Error("Expected this to be world")
33+
}
34+
})
35+
After(function() {
36+
if (!this.isWorld()) {
37+
throw Error("Expected this to be world")
38+
}
39+
})
40+
"""
41+
When I run cucumber-js
42+
Then it passes
43+
44+
Scenario: World is this in BeforeStep hooks
45+
Given a file named "features/support/hooks.js" with:
46+
"""
47+
const {BeforeStep } = require('@cucumber/cucumber')
48+
BeforeStep(function() {
49+
if (!this.isWorld()) {
50+
throw Error("Expected this to be world")
51+
}
52+
})
53+
"""
54+
When I run cucumber-js
55+
Then it passes
56+
57+
Scenario: World is this in AfterStep hooks
58+
Given a file named "features/support/hooks.js" with:
59+
"""
60+
const {AfterStep } = require('@cucumber/cucumber')
61+
AfterStep(function() {
62+
if (!this.isWorld()) {
63+
throw Error("Expected this to be world")
64+
}
65+
})
66+
"""
67+
When I run cucumber-js
68+
Then it passes

src/cli/helpers_spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ function testEmitSupportCodeMessages(
6969
stepDefinitions: [],
7070
beforeTestRunHookDefinitions: [],
7171
beforeTestCaseHookDefinitions: [],
72+
beforeTestStepHookDefinitions: [],
7273
afterTestRunHookDefinitions: [],
7374
afterTestCaseHookDefinitions: [],
75+
afterTestStepHookDefinitions: [],
7476
defaultTimeout: 0,
7577
parameterTypeRegistry: new ParameterTypeRegistry(),
7678
undefinedParameterTypes: [],

src/formatter/helpers/summary_helpers_spec.ts

+43
Original file line numberDiff line numberDiff line change
@@ -351,5 +351,48 @@ describe('SummaryHelpers', () => {
351351
)
352352
})
353353
})
354+
355+
describe('with one passing scenario with one step and a beforeStep and afterStep hook', () => {
356+
it('outputs the duration as `0m24.000s (executing steps: 0m24.000s)`', async () => {
357+
// Arrange
358+
const sourceData = [
359+
'Feature: a',
360+
'Scenario: b',
361+
'Given a passing step',
362+
].join('\n')
363+
const supportCodeLibrary = buildSupportCodeLibrary(
364+
({ Given, BeforeStep, AfterStep }) => {
365+
Given('a passing step', () => {
366+
clock.tick(12.3 * 1000)
367+
})
368+
BeforeStep(() => {
369+
clock.tick(5 * 1000)
370+
})
371+
AfterStep(() => {
372+
clock.tick(6.7 * 1000)
373+
})
374+
}
375+
)
376+
377+
// Act
378+
const output = await testFormatSummary({
379+
sourceData,
380+
supportCodeLibrary,
381+
testRunFinished: messages.TestRunFinished.fromObject({
382+
timestamp: {
383+
nanos: 0,
384+
seconds: 24,
385+
},
386+
}),
387+
})
388+
389+
// Assert
390+
expect(output).to.contain(
391+
'1 scenario (1 passed)\n' +
392+
'1 step (1 passed)\n' +
393+
'0m24.000s (executing steps: 0m24.000s)\n'
394+
)
395+
})
396+
})
354397
})
355398
})

src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export { default as UsageFormatter } from './formatter/usage_formatter'
2222
export { default as UsageJsonFormatter } from './formatter/usage_json_formatter'
2323
export { formatterHelpers }
2424

25-
// Support Code Fuctions
25+
// Support Code Functions
2626
const { methods } = supportCodeLibraryBuilder
2727
export const After = methods.After
2828
export const AfterAll = methods.AfterAll
29+
export const AfterStep = methods.AfterStep
2930
export const Before = methods.Before
3031
export const BeforeAll = methods.BeforeAll
32+
export const BeforeStep = methods.BeforeStep
3133
export const defineParameterType = methods.defineParameterType
3234
export const defineStep = methods.defineStep
3335
export const Given = methods.Given
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { PickleTagFilter } from '../pickle_filter'
2+
import Definition, {
3+
IDefinition,
4+
IGetInvocationDataResponse,
5+
IGetInvocationDataRequest,
6+
IDefinitionParameters,
7+
IHookDefinitionOptions,
8+
} from './definition'
9+
import { messages } from '@cucumber/messages'
10+
11+
export default class TestStepHookDefinition
12+
extends Definition
13+
implements IDefinition {
14+
public readonly tagExpression: string
15+
private readonly pickleTagFilter: PickleTagFilter
16+
17+
constructor(data: IDefinitionParameters<IHookDefinitionOptions>) {
18+
super(data)
19+
this.tagExpression = data.options.tags
20+
this.pickleTagFilter = new PickleTagFilter(data.options.tags)
21+
}
22+
23+
appliesToTestCase(pickle: messages.IPickle): boolean {
24+
return this.pickleTagFilter.matchesAllTagExpressions(pickle)
25+
}
26+
27+
async getInvocationParameters({
28+
hookParameter,
29+
}: IGetInvocationDataRequest): Promise<IGetInvocationDataResponse> {
30+
return await Promise.resolve({
31+
getInvalidCodeLengthMessage: () =>
32+
this.buildInvalidCodeLengthMessage('0 or 1', '2'),
33+
parameters: [hookParameter],
34+
validCodeLengths: [0, 1, 2],
35+
})
36+
}
37+
}

0 commit comments

Comments
 (0)