Skip to content

Commit

Permalink
Add support for named hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
badeball committed Nov 2, 2023
1 parent ba4f10b commit 0a5ea70
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Other changes:

- Scenario hooks (`Before(..)` and `After(..)`) are now invoked with an object containing a bunch of relevant data. This is in line with how cucumber-js behaves.

- Hooks may now be optionally named. This is in line with how cucumber-js behaves.

- Omit outputting internal task to the command log when using `attach(..)`.

## v18.0.6
Expand Down
49 changes: 41 additions & 8 deletions docs/cucumber-basics.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
[← Back to documentation](readme.md)

# Expressions
# Table of Contents <!-- omit from toc -->

- [Step definitions](#step-definitions)
- [Expressions](#expressions)
- [Arguments](#arguments)
- [Custom parameter types](#custom-parameter-types)
- [Pending steps](#pending-steps)
- [Skipped steps](#skipped-steps)
- [Nested steps](#nested-steps)
- [Hooks](#hooks)
- [Scenario hooks](#scenario-hooks)
- [Step hooks](#step-hooks)
- [Named hooks](#named-hooks)

# Step definitions

## Expressions

A step definition’s *expression* can either be a regular expression or a [cucumber expression](https://github.com/cucumber/cucumber-expressions#readme). The examples in this section use cucumber expressions. If you prefer to use regular expressions, each *capture group* from the match will be passed as arguments to the step definition’s function.

```ts
Given("I have {int} cukes in my belly", (cukes: number) => {});
```

# Arguments
## Arguments

Steps can be accompanied by [doc strings](https://cucumber.io/docs/gherkin/reference/#doc-strings) or [data tables](https://cucumber.io/docs/gherkin/reference/#data-tables), both which will be passed to the step definition as the last argument, as shown below.

Expand All @@ -34,7 +50,7 @@ Given(/^a table step$/, (table: DataTable) => {

See [here](https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/data_table_interface.md) for `DataTable`'s interface.

# Custom parameter types
## Custom parameter types

Custom parameter types can be registered using `defineParameterType()`. They share the same scope as tests and you can invoke `defineParameterType()` anywhere you define steps, though the order of definition is unimportant. The table below explains the various arguments you can pass when defining a parameter type.

Expand All @@ -44,7 +60,7 @@ Custom parameter types can be registered using `defineParameterType()`. They sha
| `regexp` | A regexp that will match the parameter. May include capture groups.
| `transformer` | A function or method that transforms the match from the regexp. Must have arity 1 if the regexp doesn't have any capture groups. Otherwise the arity must match the number of capture groups in `regexp`. |

# Pending steps
## Pending steps

You can return `"pending"` from a step defintion or a chain to mark a step as pending. This will halt the execution and Cypress will report the test as "skipped". This is generally used for marking steps as "unimplemented" and allows you to commit unfinished work without breaking the test suite.

Expand All @@ -66,7 +82,7 @@ When("a step", () => {
});
```

# Skipped steps
## Skipped steps

You can return `"skipped"` from a step defintion or a chain to mark a step as pending. This will halt the execution and Cypress will report the test as "skipped". This however is generally used for conditionally short circuiting a test.

Expand All @@ -88,7 +104,7 @@ When("a step", () => {
});
```

# Nested steps
## Nested steps

You can invoke other steps from a step using `Step()`, as shown below.

Expand Down Expand Up @@ -123,7 +139,11 @@ When("I fill in the entire form", function () {
});
```

# Scenario hooks
# Hooks

There are two types of hooks, scenario hooks and step hooks, each explained below.

## Scenario hooks

`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`.

Expand Down Expand Up @@ -154,7 +174,7 @@ Before(function ({ pickle, gherkinDocument, testCaseStartedId }) {
});
```

# Step hooks
## Step hooks

`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.

Expand Down Expand Up @@ -186,3 +206,16 @@ BeforeStep(function ({ pickle, pickleStep, gherkinDocument, testCaseStartedId, t
```

[^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).

## Named hooks

Both scenario hooks and step hooks can optionally be named. Names are displayed in the command log, as well as in [messages reports](messages-report.md).

```ts
import { Before, BeforeStep, After, AfterStep } from "@badeball/cypress-cucumber-preprocessor";

Before({ name: "foo" }, function () {});
BeforeStep({ name: "bar" }, function () {});
After({ name: "baz" }, function () {});
AfterStep({ name: "qux" }, function () {});
```
161 changes: 161 additions & 0 deletions features/hooks_name.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
Feature: named hooks

Background:
Given a file named "cypress/e2e/a.feature" with:
"""
@foo
Feature: a feature
Scenario: a scenario
Given a step
"""
Given a file named "cypress/support/step_definitions/steps.js" with:
"""
const { Given } = require("@badeball/cypress-cucumber-preprocessor");
Given("a step", () => {});
"""

Rule: named hooks should be displayed appropriately in the command log

Scenario: Before hook (untagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { Before } = require("@badeball/cypress-cucumber-preprocessor");
Before({ name: "foo" }, () => {
cy.expectCommandLogEntry({
method: "Before",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: Before hook (tagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { Before } = require("@badeball/cypress-cucumber-preprocessor");
Before({ name: "foo", tags: "@foo" }, () => {
cy.expectCommandLogEntry({
method: "Before",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: After hook (untagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { After } = require("@badeball/cypress-cucumber-preprocessor");
After({ name: "foo" }, () => {
cy.expectCommandLogEntry({
method: "After",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: After hook (tagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { After } = require("@badeball/cypress-cucumber-preprocessor");
After({ name: "foo", tags: "@foo" }, () => {
cy.expectCommandLogEntry({
method: "After",
message: "foo (@foo)"
});
});
"""
When I run cypress
Then it passes

Scenario: BeforeStep hook (untagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { BeforeStep } = require("@badeball/cypress-cucumber-preprocessor");
BeforeStep({ name: "foo" }, () => {
cy.expectCommandLogEntry({
method: "BeforeStep",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: BeforeStep hook (tagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { BeforeStep } = require("@badeball/cypress-cucumber-preprocessor");
BeforeStep({ name: "foo", tags: "@foo" }, () => {
cy.expectCommandLogEntry({
method: "BeforeStep",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: AfterStep hook (untagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { AfterStep } = require("@badeball/cypress-cucumber-preprocessor");
AfterStep({ name: "foo" }, () => {
cy.expectCommandLogEntry({
method: "AfterStep",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Scenario: AfterStep hook (tagged)
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { AfterStep } = require("@badeball/cypress-cucumber-preprocessor");
AfterStep({ name: "foo", tags: "@foo" }, () => {
cy.expectCommandLogEntry({
method: "AfterStep",
message: "foo"
});
});
"""
When I run cypress
Then it passes

Rule: some named hooks should be reported appropriately

Background:
Given additional preprocessor configuration
"""
{
"messages": {
"enabled": true
}
}
"""

Scenario: Before hook
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { Before } = require("@badeball/cypress-cucumber-preprocessor");
Before({ name: "foo" }, () => {});
"""
When I run cypress
Then it passes
And the message report should contain a hook named "foo"

Scenario: After hook
Given a file named "cypress/support/step_definitions/hooks.js" with:
"""
const { After } = require("@badeball/cypress-cucumber-preprocessor");
After({ name: "foo" }, () => {});
"""
When I run cypress
Then it passes
And the message report should contain a hook named "foo"
9 changes: 3 additions & 6 deletions features/issues/922.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ Feature: visualizing hook with filter
Before(() => {})
Before({ tags: "@foo or @bar" }, () => {})
Given("a step", function() {
cy.then(() => {}).should(() => {
expect(
Cypress.$(top.document).find(
".command-info:has(> .command-method:contains('Before')) .command-message-text:contains('@foo or @bar')"
)
).to.exist;
cy.expectCommandLogEntry({
method: "Before",
message: "@foo or @bar"
});
})
"""
Expand Down
6 changes: 1 addition & 5 deletions features/step_definitions/html_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { JSDOM } from "jsdom";
import path from "path";
import { promises as fs } from "fs";
import assert from "assert";

function assertAndReturn<T>(value: T | null | undefined, msg?: string): T {
assert(value, msg);
return value;
}
import { assertAndReturn } from "../support/helpers";

Then("there should be a HTML report", async function () {
await assert.doesNotReject(
Expand Down
15 changes: 15 additions & 0 deletions features/step_definitions/messages_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { promises as fs } from "fs";
import assert from "assert";
import { toByteArray } from "base64-js";
import { PNG } from "pngjs";
import { assertAndReturn } from "../support/helpers";

function isObject(object: any): object is object {
return typeof object === "object" && object != null;
Expand Down Expand Up @@ -266,3 +267,17 @@ Then(
}
}
);

Then(
"the message report should contain a hook named {string}",
async function (name) {
const messages = await readMessagesReport(this.tmpDir);

const hook = assertAndReturn(
messages.map((message) => message.hook).find((hook) => hook),
"Expected to find a hook among messages"
);

assert.equal(hook.name, name);
}
);
9 changes: 9 additions & 0 deletions features/support/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import path from "path";
import { promises as fs } from "fs";
import assert from "assert";

export async function writeFile(filePath: string, fileContent: string) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, fileContent);
}

export function assertAndReturn<T>(
value: T | null | undefined,
msg?: string
): T {
assert(value, msg);
return value;
}
12 changes: 11 additions & 1 deletion features/support/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ Before(async function ({ gherkinDocument, pickle }) {

await fs.rm(this.tmpDir, { recursive: true, force: true });

await writeFile(path.join(this.tmpDir, "cypress", "support", "e2e.js"), "");
await writeFile(
path.join(this.tmpDir, "cypress", "support", "e2e.js"),
`
Cypress.Commands.add("expectCommandLogEntry", ({ method, message }) => {
const selector = \`.command-info:has(> .command-method:contains('\${method}')) .command-message-text:contains('\${message}')\`;
cy.then(() => {}).should(() => {
expect(Cypress.$(top.document).find(selector)).to.exist;
});
});
`
);

await writeFile(
path.join(this.tmpDir, "cypress.config.js"),
Expand Down
Loading

0 comments on commit 0a5ea70

Please sign in to comment.