Skip to content

Commit 3569210

Browse files
committed
Add BeforeStep & AfterStep
This fixes #847 [1]. [1] #847
1 parent afb4323 commit 3569210

File tree

10 files changed

+401
-8
lines changed

10 files changed

+401
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## Unreleased
6+
7+
- Add BeforeStep and AfterStep hooks, fixes [#847](https://github.com/badeball/cypress-cucumber-preprocessor/issues/847).
8+
59
## v17.1.1
610

711
- Allow generation of JSON reports with hooks (After / Before) even if `baseUrl` is undefined, fixes [#1017](https://github.com/badeball/cypress-cucumber-preprocessor/issues/1017).

docs/cucumber-basics.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ When("I fill in the entire form", function () {
101101
});
102102
```
103103

104-
# Hooks
104+
# Scenario hooks
105105

106106
`Before()` and `After()` is similar to Cypress' `beforeEach()` and `afterEach()`, but they can be selected to conditionally run based on the tags of each scenario, as shown below. Furthermore, failure in these hooks does **not** result in remaining tests being skipped. This is contrary to Cypress' `beforeEach` and `afterEach`.
107107

@@ -128,4 +128,35 @@ Before({ tags: "@foo or @bar" }, function () {
128128
});
129129
```
130130

131+
# Step hooks
132+
133+
`BeforeStep()` and `AfterStep()` are hooks invoked before and after each step, respectively. These too can be selected to conditionally run based on the tags of each scenario, as shown below.
134+
135+
> **Note**
136+
> Contrary to how cucumber-js works, these `AfterStep()` hooks **does not** run if your step fails[^1].
137+
138+
```ts
139+
import { BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
140+
141+
BeforeStep(function (options) {
142+
// This hook will be executed before all steps.
143+
});
144+
145+
BeforeStep({ tags: "@foo" }, function () {
146+
// This hook will be executed before steps in scenarios tagged with @foo.
147+
});
148+
149+
BeforeStep({ tags: "@foo and @bar" }, function () {
150+
// This hook will be executed before steps in scenarios tagged with @foo and @bar.
151+
});
152+
153+
BeforeStep({ tags: "@foo or @bar" }, function () {
154+
// This hook will be executed before steps in scenarios tagged with @foo or @bar.
155+
});
156+
157+
BeforeStep(function ({ pickle, pickleStep, gherkinDocument, result, testCaseStartedId, testStepId }) {
158+
// Step hooks are invoked with an object containing a bunch of relevant data.
159+
});
160+
```
161+
131162
[^1]: This discrepancy between the preprocessor and cucumber-js is currently considered to be unsolvable, as explained [here](https://github.com/badeball/cypress-cucumber-preprocessor/issues/824#issuecomment-1561492281).

features/reporters/json.feature

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,57 @@ Feature: JSON formatter
232232
Then it fails
233233
And there should be a JSON output similar to "fixtures/failing-after.json"
234234

235+
Rule: step hooks affects the result of the current step
236+
Background:
237+
Given additional Cypress configuration
238+
"""
239+
{
240+
"screenshotOnRunFailure": false
241+
}
242+
"""
243+
244+
Scenario: failing BeforeStep
245+
Given a file named "cypress/e2e/a.feature" with:
246+
"""
247+
Feature: a feature
248+
Scenario: a scenario
249+
Given a failing step
250+
And another step
251+
"""
252+
And a file named "cypress/support/step_definitions/steps.js" with:
253+
"""
254+
const { BeforeStep, Given } = require("@badeball/cypress-cucumber-preprocessor");
255+
BeforeStep(function() {
256+
throw "some error"
257+
})
258+
Given("a failing step", function() {})
259+
Given("another step", function () {})
260+
"""
261+
When I run cypress
262+
Then it fails
263+
And there should be a JSON output similar to "fixtures/failing-step.json"
264+
265+
Scenario: failing AfterStep
266+
Given a file named "cypress/e2e/a.feature" with:
267+
"""
268+
Feature: a feature
269+
Scenario: a scenario
270+
Given a failing step
271+
And another step
272+
"""
273+
And a file named "cypress/support/step_definitions/steps.js" with:
274+
"""
275+
const { AfterStep, Given } = require("@badeball/cypress-cucumber-preprocessor");
276+
AfterStep(function() {
277+
throw "some error"
278+
})
279+
Given("a failing step", function() {})
280+
Given("another step", function () {})
281+
"""
282+
When I run cypress
283+
Then it fails
284+
And there should be a JSON output similar to "fixtures/failing-step.json"
285+
235286
Rule: it should contain screenshots captured during a test
236287
Scenario: explicit screenshot
237288
Given a file named "cypress/e2e/a.feature" with:

features/reporters/messages.feature

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,57 @@ Feature: messages report
233233
Then it fails
234234
And there should be a messages similar to "fixtures/failing-after.ndjson"
235235

236+
Rule: step hooks affects the result of the current step
237+
Background:
238+
Given additional Cypress configuration
239+
"""
240+
{
241+
"screenshotOnRunFailure": false
242+
}
243+
"""
244+
245+
Scenario: failing BeforeStep
246+
Given a file named "cypress/e2e/a.feature" with:
247+
"""
248+
Feature: a feature
249+
Scenario: a scenario
250+
Given a failing step
251+
And another step
252+
"""
253+
And a file named "cypress/support/step_definitions/steps.js" with:
254+
"""
255+
const { BeforeStep, Given } = require("@badeball/cypress-cucumber-preprocessor");
256+
BeforeStep(function() {
257+
throw "some error"
258+
})
259+
Given("a failing step", function() {})
260+
Given("another step", function () {})
261+
"""
262+
When I run cypress
263+
Then it fails
264+
And there should be a messages similar to "fixtures/failing-step.ndjson"
265+
266+
Scenario: failing AfterStep
267+
Given a file named "cypress/e2e/a.feature" with:
268+
"""
269+
Feature: a feature
270+
Scenario: a scenario
271+
Given a failing step
272+
And another step
273+
"""
274+
And a file named "cypress/support/step_definitions/steps.js" with:
275+
"""
276+
const { AfterStep, Given } = require("@badeball/cypress-cucumber-preprocessor");
277+
AfterStep(function() {
278+
throw "some error"
279+
})
280+
Given("a failing step", function() {})
281+
Given("another step", function () {})
282+
"""
283+
When I run cypress
284+
Then it fails
285+
And there should be a messages similar to "fixtures/failing-step.ndjson"
286+
236287
Rule: it should contain screenshots captured during a test
237288
Scenario: explicit screenshot
238289
Given a file named "cypress/e2e/a.feature" with:

lib/browser-runtime.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ import { runStepWithLogGroup } from "./helpers/cypress";
6363

6464
import { getTags } from "./helpers/environment";
6565

66+
import { IStepHookParameter } from "./public-member-types";
67+
6668
type Node = ReturnType<typeof parse>;
6769

6870
interface CompositionContext {
@@ -437,6 +439,16 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) {
437439
return cy.wrap(start, { log: false });
438440
})
439441
.then((start) => {
442+
const beforeStepHooks = registry.resolveBeforeStepHooks(tags);
443+
const afterStepHooks = registry.resolveAfterStepHooks(tags);
444+
const options: IStepHookParameter = {
445+
pickle,
446+
pickleStep,
447+
gherkinDocument,
448+
testCaseStartedId,
449+
testStepId: pickleStep.id,
450+
};
451+
440452
try {
441453
return runStepWithLogGroup({
442454
keyword: assertAndReturn(
@@ -445,7 +457,23 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) {
445457
),
446458
argument,
447459
text,
448-
fn: () => registry.runStepDefininition(this, text, argument),
460+
fn: () => {
461+
for (const beforeStepHook of beforeStepHooks) {
462+
registry.runStepHook(this, beforeStepHook, options);
463+
}
464+
465+
const result = registry.runStepDefininition(
466+
this,
467+
text,
468+
argument
469+
);
470+
471+
for (const afterStepHook of afterStepHooks.reverse()) {
472+
registry.runStepHook(this, afterStepHook, options);
473+
}
474+
475+
return result;
476+
},
449477
}).then((result) => {
450478
return {
451479
start,

lib/entrypoint-browser.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { getRegistry } from "./registry";
2525

2626
import {
2727
IHookBody,
28+
IStepHookBody,
2829
IParameterTypeDefinition,
2930
IStepDefinitionBody,
3031
} from "./public-member-types";
@@ -84,6 +85,36 @@ function defineAfter(
8485
}
8586
}
8687

88+
function defineBeforeStep(options: { tags?: string }, fn: IStepHookBody): void;
89+
function defineBeforeStep(fn: IStepHookBody): void;
90+
function defineBeforeStep(
91+
optionsOrFn: IStepHookBody | { tags?: string },
92+
maybeFn?: IStepHookBody
93+
) {
94+
if (typeof optionsOrFn === "function") {
95+
getRegistry().defineBeforeStep({}, optionsOrFn);
96+
} else if (typeof optionsOrFn === "object" && typeof maybeFn === "function") {
97+
getRegistry().defineBeforeStep(optionsOrFn, maybeFn);
98+
} else {
99+
throw new Error("Unexpected argument for Before hook");
100+
}
101+
}
102+
103+
function defineAfterStep(options: { tags?: string }, fn: IStepHookBody): void;
104+
function defineAfterStep(fn: IStepHookBody): void;
105+
function defineAfterStep(
106+
optionsOrFn: IStepHookBody | { tags?: string },
107+
maybeFn?: IStepHookBody
108+
) {
109+
if (typeof optionsOrFn === "function") {
110+
getRegistry().defineAfterStep({}, optionsOrFn);
111+
} else if (typeof optionsOrFn === "object" && typeof maybeFn === "function") {
112+
getRegistry().defineAfterStep(optionsOrFn, maybeFn);
113+
} else {
114+
throw new Error("Unexpected argument for After hook");
115+
}
116+
}
117+
87118
function createStringAttachment(
88119
data: string,
89120
mediaType: string,
@@ -161,4 +192,6 @@ export {
161192
defineParameterType,
162193
defineBefore as Before,
163194
defineAfter as After,
195+
defineBeforeStep as BeforeStep,
196+
defineAfterStep as AfterStep,
164197
};

lib/entrypoint-node.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DataTable from "./data_table";
22

33
import {
44
IHookBody,
5+
IStepHookBody,
56
IParameterTypeDefinition,
67
IStepDefinitionBody,
78
} from "./public-member-types";
@@ -98,4 +99,26 @@ export function After(
9899
throw createUnimplemented();
99100
}
100101

102+
export function BeforeStep(options: { tags?: string }, fn: IStepHookBody): void;
103+
export function BeforeStep(fn: IStepHookBody): void;
104+
export function BeforeStep(
105+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
106+
optionsOrFn: IStepHookBody | { tags?: string },
107+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
108+
maybeFn?: IStepHookBody
109+
) {
110+
throw createUnimplemented();
111+
}
112+
113+
export function AfterStep(options: { tags?: string }, fn: IStepHookBody): void;
114+
export function AfterStep(fn: IStepHookBody): void;
115+
export function AfterStep(
116+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
117+
optionsOrFn: IStepHookBody | { tags?: string },
118+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
119+
maybeFn?: IStepHookBody
120+
) {
121+
throw createUnimplemented();
122+
}
123+
101124
export { default as DataTable } from "./data_table";

lib/public-member-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as messages from "@cucumber/messages";
2+
13
export interface IParameterTypeDefinition<T, C extends Mocha.Context> {
24
name: string;
35
regexp: RegExp;
@@ -8,6 +10,18 @@ export interface IHookBody {
810
(this: Mocha.Context): void;
911
}
1012

13+
export interface IStepHookParameter {
14+
pickle: messages.Pickle;
15+
pickleStep: messages.PickleStep;
16+
gherkinDocument: messages.GherkinDocument;
17+
testCaseStartedId: string;
18+
testStepId: string;
19+
}
20+
21+
export interface IStepHookBody {
22+
(this: Mocha.Context, options: IStepHookParameter): void;
23+
}
24+
1125
export interface IStepDefinitionBody<
1226
T extends unknown[],
1327
C extends Mocha.Context

0 commit comments

Comments
 (0)