e2e-test-runner is a ready-to-use E2E test runner that leverages Cypress and Cucumber.
It provides a collection of pre-implemented reusable steps to simplify writing and maintaining end-to-end tests for your applications.
- π§ͺ Predefined Cucumber steps for HTTP requests and assertions
- π§° Built-in context system with template rendering (via Nunjucks)
- βοΈ Compatible with Cypress and Cucumber ecosystem
- π Run tests via Node or Docker
You can pull and run the prebuilt image directly from Docker Hub:
docker pull vincentmoittie/e2e-test-runner:latestgit clone https://github.com/zorin95670/e2e-test-runner e2eMake sure to include e2e in your .gitignore if it's only for local use.
git submodule add https://github.com/zorin95670/e2e-test-runner e2eThen install dependencies:
cd e2e
npm installThe test runner requires the environment variable CYPRESS_FEATURES_PATH.
- It must point to the directory containing your
.featurefiles. - The path should be relative to the current working directory (i.e., where
npm run startis executed). - For example, in a Java project structure, if the test runner is executed from the
e2efolder, the correct value would typically be:
CYPRESS_FEATURES_PATH=../src/test/resources/featuresYou must define this variable in a .env file or inject it at runtime.
To run the test runner locally and load environment variables from a .env file:
- Install
dotenv-cliglobally:
npm install -g dotenv-cli- Run the test runner:
dotenv -e ../.env -- npm run start
# With ui
dotenv -e ../.env -- npm run start:uiTo run the test runner in a Docker container, make sure to:
- Provide access to the
.envfile. - Set the timezone (e.g.,
Europe/Paris) using theTZenvironment variable. - Connect the container to the appropriate Docker network (e.g., to communicate with your application under test).
Example:
docker run --rm \
--env-file .env \
--env TZ=Europe/Paris \
--network my-app-network \
-v "$(pwd)/src/test/resources/features":/app/src/test/resources/features \
vincentmoittie:e2e-test-runner:latestOr one-line version:
docker run --rm --env-file .env --env TZ=Europe/Paris --network my-app-network -v "$(pwd)/src/test/resources/features":/app/src/test/resources/features vincentmoittie:e2e-test-runner:latestReplace
my-app-networkwith the name of the Docker network your application is running on.
To run the test runner in a Docker container, make sure to:
- Mount the project directory.
- Provide access to the
.envfile. - Set the timezone (e.g.,
Europe/Paris) using theTZenvironment variable. - Connect the container to the appropriate Docker network (e.g., to communicate with your application under test).
Example:
docker build e2e -t e2e-test-runner
docker run --rm \
--env-file .env \
--env TZ=Europe/Paris \
--network my-app-network \
-v "$(pwd)/src/test/resources/features":/app/src/test/resources/features \
e2e-test-runnerOr one-line version:
docker run --rm --env-file .env --env TZ=Europe/Paris --network my-app-network -v "$(pwd)/src/test/resources/features":/app/src/test/resources/features e2e-test-runnerReplace
my-app-networkwith the name of the Docker network your application is running on.
The test runner supports writing tests in Gherkin using .feature files.
Feature: Basic HTTP check
Scenario: GET Google homepage
When I request "https://www.google.com" with method "GET"
Then I expect status code is 200You can use Nunjucks templating syntax inside step values. This allows dynamic content, especially useful when injecting data from environment variables or shared context.
Given I set http header "Authorization" with "Bearer {{ ctx.AUTH_TOKEN }}"A custom json filter has been added to Nunjucks to properly serialize objects into formatted JSON strings.
Then I log "{{ response.body | json }}"This is useful for debugging or displaying formatted JSON output directly from the response.
This test runner provides a built-in context system to share data between steps. It is designed to simplify dynamic value handling, templating, and state tracking during test execution.
| Key | Description |
|---|---|
env |
Contains all environment variables. Useful for configuration and templating. |
ctx |
A customizable object used to store any custom data needed during the test flow. Reset before each scenario. |
response |
Stores the last HTTP response. Reset before each scenario. |
httpHeaders |
Stores all HTTP headers set via step definitions. Reset before each scenario. |
Example Usage
You can also use the context in templated strings (via Nunjucks):
When I request "{{ env.API_URL }}/users/{{ ctx.userId }}" with method "GET"Certain steps allow you to specify a type to interpret or convert values properly.
The following types are supported:
| Type | Description |
|---|---|
string |
Default type. Values are kept as-is. |
integer |
Converts the value to a JavaScript integer (e.g., "42" β 42). |
float |
Converts the value to a JavaScript float (e.g., "3.14" β 3.14). |
boolean |
Converts the value to a boolean ("true" β true, "false" β false). |
json |
Parses the value as a JSON object (e.g., "{ \"foo\": 123 }" β { foo: 123 }). |
π These types are used to ensure that context variables, assertions, and request parameters behave as expected during test execution.
In some steps like:
When I request "<url>" with method "<method>" with query parametersYou can define a DataTable in your Gherkin file to pass query parameters dynamically.
The table must include at least key and value, and may optionally include a type column:
When I request "/api/search" with method "GET" with query parameters
| key | value | type |
| q | banana | string |
| limit | 10 | integer |
| exact | true | boolean || Column | Required | Description |
|---|---|---|
key |
β | The name of the query parameter. |
value |
β | The value to assign to the key. |
type |
β | Optional type conversion (see supported types above). Default: string |
β
The table is evaluated with Nunjucks, so you can dynamically reference values like {{ ctx.foo }}.
You can use tags to control which scenarios or features to run during test execution:
Use @skip above a scenario to skip it.
Feature: User login
@skip
Scenario: Valid credentials
Given I visit the login page
When I enter valid credentials
Then I should be redirected to the dashboardUse @only above a scenario to run only that one. All others will be ignored.
Feature: User login
@only
Scenario: Invalid credentials
Given I visit the login page
When I enter invalid credentials
Then I should see an error messageUse @skip above the Feature: line to skip all scenarios in that file.
@skip
Feature: Password recovery
Scenario: Request password reset
Given I visit the forgot password page
When I enter my email
Then I should receive a reset email- Log a value
Then I log "<value>"
# Example:
Then I log "{{ response.status }}"- Wait a duration
Then I wait <seconds>s
#Example:
Then I wait 2s- Store a value in context
Then I store "<key>" as "<value>" in context
# Example:
Then I store "userId" as "{{ response.body.id }}" in context- Store a raw value in context
Then I store as "<value>":
"""
My data
"""
# Example:
Then I store as "data":
"""
My data
"""- Basic request
When I request "<url>" with method "<method>"
# Example:
When I request "https://api.example.com/items" with method "GET"- Request with query parameters
When I request "<url>" with method "<method>" with query parameters
| key | value | type |
| userId | 123 | integer |
# Example:
When I request "https://api.example.com/users" with method "GET" with query parameters
| key | value | type |
| userId | 123 | integer |- Request with raw body
When I request "<url>" with method "<method>" with body:
"""
{ "name": "John" }
"""
# Example:
When I request "https://api.example.com/users" with method "POST" with body:
"""
{ "name": "John" }
"""- Set HTTP header
Given I set http header "<key>" with "<value>"
# Example:
Given I set http header "Authorization" with "Bearer abc123"- Check status code
Then I expect status code is <code>
# Example:
Then I expect status code is 200- Compare templated values
Then I expect "<value>" is empty
Then I expect "<value>" is not empty
Then I expect "<value>" is "<expected>"
Then I expect "<value>" is not "<expected>"
Then I expect "<value>" contains "<expected>"
Then I expect "<value>" not contains "<expected>"
# Example:
Then I expect "{{ response.body }}" is empty
Then I expect "{{ response.body }}" is not empty
Then I expect "{{ response.status }}" is "200"
Then I expect "{{ response.status }}" is not "200"
Then I expect "{{ response.body.name }}" contains "test"
Then I expect "{{ response.body.name }}" not contains "test"- Compare templated values with type
Then I expect "<value>" is "<expected>" as "<type>"
# Example:
Then I expect "{{ response.status }}" is "200" as "integer"- Check length of value
Then I expect "<value>" to have length <number>
# Example:
Then I expect "{{ response.body.users }}" to have length 5- Check length with type
Then I expect "<value>" as "<type>" to have length <number>
# Example:
Then I expect "{{ response.body.users }}" as "json" to have length 5- One resource equals to a value
Then I expect one resource of "<value>" equals to "<expected>"
# Example:
Then I expect one resource of "{{ response.body.users }}" equals to "John"- One resource equals with type
Then I expect one resource of "<value>" equals to "<expected>" as "<type>"
# Example:
Then I expect one resource of "{{ response.body.users }}" equals to "123" as "integer"- Resource field match expected
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>"
# Example:
Then I expect one resource of "{{ response.body.users }}" contains "name" equals to "Alice"- Resource field match expected with type
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>" as "<type>"
# Example:
Then I expect one resource of "{{ response.body.users }}" contains "age" equals to "30" as "integer"- Set a value in LocalStorage
Given I set in localstorage field "<key>" with "<value>"
# Example:
Given I set in localstorage field "token" with "abc123"- Assert a value in LocalStorage
Then I expect localstorage field "<key>" is "<expected>"
# Example:
Then I expect localstorage field "token" is "abc123"- Delete a value from LocalStorage
Then I delete "<key>" in localstorage
# Example:
Then I delete "token" in localstorage- Copy LocalStorage field to context
Then I set localstorage field "<localStorageField>" to context field "<contextField>"
# Example:
Then I set localstorage field "token" to context field "userToken"- Copy LocalStorage field to context with type conversion
Then I set localstorage field "<localStorageField>" to context field "<contextField>" as "<type>"
# Example:
Then I set localstorage field "isAdmin" to context field "adminFlag" as "boolean"- Visit a URL
Given I visit "<url>"
# Example:
Given I visit "{{ env.baseUrl }}/login"- Reload a specific URL
Given I reload to "<url>"
# Example:
Given I reload to "{{ env.baseUrl }}/dashboard"- Expect current URL is exactly
Then I expect current url is "<url>"
# Example:
Then I expect current url is "{{ env.baseUrl }}/home"- Expect current URL contains a string
Then I expect current url contains "<url>"
# Example:
Then I expect current url contains "/home"- Expect current URL matches a pattern
Then I expect current url matches "<regex>"
# Example:
Then I expect current url matches ".*\\/profile\\/\\d+$"- Expect current URL is no longer a specific URL
Then I expect the current URL no longer is "<url>"
# Example:
Then I expect the current URL no longer is "{{ env.baseUrl }}/splash"- Expect current URL no longer contains a string
Then I expect the current URL no longer contains "<text>"
# Example:
Then I expect the current URL no longer contains "/splash"- Expect current URL no longer matches a pattern
Then I expect the current URL no longer matches "<regex>"
# Example:
Then I expect the current URL no longer matches ".*\\/splash$"- Click on an element
When I click on "<selector>"
# Example:
When I click on "#submit-button"- Force click on an element (ignores visibility)
When I force click on "<selector>"
# Example:
When I force click on ".dropdown-toggle"- Double-click on an element
When I double click on "<selector>"
# Example:
When I double click on "#editable-cell"- Scroll to a position inside an element
When I scroll to "<position>" into "<selector>"
# Example:
When I scroll to "bottom" into ".scrollable-container"- Hover an element to make it visible
When I hover "<selector>" to make it visible
# Example:
When I hover ".tooltip-trigger" to make it visible- Drag an element onto another
When I drag "<originSelector>" onto "<destinationSelector>"
# Example:
When I drag "#item" onto "#dropzone"- Drag an element by a specific offset
When I drag "<selector>" of <x>,<y>
# Example:
When I drag "#slider" of 50,0- Move an element by offset
When I move "<selector>" of <x>,<y>
# Example:
When I move "#box" of 20,30- Select an option inside a dropdown or menu
When I select "<option>" in "<selector>"
# Example:
When I select ".option-2" in ".dropdown-menu"- Expect element to exist
Then I expect the HTML element "<selector>" exists- Expect element to not exist
Then I expect the HTML element "<selector>" not exists- Expect element to be visible
Then I expect the HTML element "<selector>" to be visible- Expect element to be hidden
Then I expect the HTML element "<selector>" to be hidden- Expect element to be disabled
Then I expect the HTML element "<selector>" to be disabled- Expect element to be enabled
Then I expect the HTML element "<selector>" to be enabled- Expect element to have exact width
Then I expect the HTML element "<selector>" width is <width>
# Example:
Then I expect the HTML element "#image" width is 200- Expect element to have exact height
Then I expect the HTML element "<selector>" height is <height>
# Example:
Then I expect the HTML element "#container" height is 400- Expect element to be at a specific position
Then I expect the HTML element "<selector>" to be at position <x>,<y>
# Example:
Then I expect the HTML element "#popup" to be at position 100,200- Expect element to have a specific attribute and value
Then I expect the HTML element "<selector>" to have attribute "<attribute>" with value "<value>"
# Example:
Then I expect the HTML element "#logo" to have attribute "alt" with value "Company Logo"- Expect element to contain specific text
Then I expect the HTML element "<selector>" contains "<text>"
# Example:
Then I expect the HTML element "#message" contains "Welcome!"- Expect element to not contain specific text
Then I expect the HTML element "<selector>" not contains "<text>"
# Example:
Then I expect the HTML element "#message" not contains "Welcome!"- Expect element to have a specific value
Then I expect the HTML element "<selector>" to have value "<value>"
# Example:
Then I expect the HTML element "#username" to have value "john.doe"- Expect element to appear a certain number of times
Then I expect the HTML element "<selector>" appear <count> time(s) on screen
# Example:
Then I expect the HTML element ".card" appear 3 time(s) on screen- Expect element is checked
Then I expect the HTML element "<selector>" is checked
# Example:
Then I expect the HTML element ".checkbox" is checked- Expect element is not checked
Then I expect the HTML element "<selector>" is not checked
# Example:
Then I expect the HTML element ".checkbox" is not checked- Clear the text inside an element
Then I clear the text in the HTML element "<selector>"
# Example:
Then I clear the text in the HTML element "#search-box"- Set a specific text inside an element
Then I set the text "<text>" in the HTML element "<selector>"
# Example:
Then I set the text "admin" in the HTML element "#username"- Set viewport size (useful for responsive tests)
Given I set the viewport size to <width> px by <height> px
# Example:
Given I set the viewport size to 1280 px by 720 px- Setup Kafka client with clientId and broker
Given I setup kafka with clientId "<clientId>" and broker "<broker>"
# Example:
Given I setup kafka with clientId "my-client" and broker "localhost:9092"- Setup Kafka producer
Given I setup kafka producer
# Example:
Given I setup kafka producer- Setup Kafka consumer with groupId
Given I setup kafka consumer with groupId "<groupId>"
# Example:
Given I setup kafka consumer with groupId "test-group"- Listen for Kafka messages on a topic
Given I listen for Kafka messages on the topic "<topic>"
# Example:
Given I listen for Kafka messages on the topic "orders"- Send kafka message
When I send a Kafka message on the topic "<topic>" with body "<body>"
# Example:
When I send a Kafka message on the topic "orders" with body "my data"- Send kafka message with raw value
When I send a Kafka message on the topic "<topic>" with body:
"""
{ "name": "John" }
"""
# Example:
When I send a Kafka message on the topic "orders" with body:
"""
{ "name": "John" }
"""- Expect a number of messages received on a Kafka topic
Then I expect {int} message(s) received on Kafka topic "<topic>"
# Example:
Then I expect 3 message(s) received on Kafka topic "orders"- Expect a message on a Kafka topic to be equal to a string
Then I expect a message on Kafka topic "<topic>" equals to "<value>"
# Example:
Then I expect a message on Kafka topic "orders" equals to "{\"orderId\":123}"- Expect a message on a Kafka topic to be equal to a provided type
Then I expect a message on Kafka topic "<topic>" equals to "<value>" as "<type>"
# Example:
Then I expect a message on Kafka topic "orders" equals to "{\"orderId\":123}" as "json"- Expect a message on a Kafka topic to be equal to a multi-line string
Then I expect a message on Kafka topic "<topic>" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""
# Example:
Then I expect a message on Kafka topic "orders" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""- Expect a message on a Kafka topic contains a substring
Then I expect a message on Kafka topic "<topic>" contains "<value>"
# Example:
Then I expect a message on Kafka topic "orders" contains "shipped"- Expect a message on a Kafka topic contains a multi-line string
Then I expect a message on Kafka topic "<topic>" contains:
"""
{
"status": "shipped"
}
"""
# Example:
Then I expect a message on Kafka topic "orders" contains:
"""
{
"status": "shipped"
}
"""- Expect a message on a Kafka topic matches a regex
Then I expect a message on Kafka topic "<topic>" matches regex "<regex>"
# Example:
Then I expect a message on Kafka topic "orders" matches regex "^\\{.*\"status\":\\s*\"shipped\".*\\}$"- Log kafka messages
Then I log kafka messages- Setup ldap client with url, bind dn and password
Given I setup Ldap with url "<url>" bind dn "<bindDn>" and password "<password>"
# Example:
Given I setup Ldap with url "ldap://localhost:389" bind dn "cn=admin,dc=company,dc=com" and password "my_password"- Search ldap results with base Dn, filter and attributes
Given I search ldap results on base dn "<baseDn>" with filter "<filter>" and attributes "<attributes>"
# Example:
Given I search ldap results on base dn "ou=users,dc=company,dc=com" with filter "[email protected]" and attributes "cn sn mail"- Expect search results to a given length
Given I expect "<expectedLength>" ldap results
# Example:
Given I expect 2 ldap results- Delete all search results previously found
Given I delete all ldap results
# Example:
Given I delete all ldap results- Add a new ldap entry with dn and with raw attributes
Given I add a ldap entry with dn "<dn>" and with attributes:
# Example:
Given I add a ldap entry with dn "uid=c1aa97e9-fdff-4b81-b468-0cb41be3e550,dc=company,dc=com" and with attributes:
"""
{
"objectClass": "top",
"objectClass": "person",
"objectClass": "organizationalPerson",
"objectClass": "inetOrgPerson",
"uid": "c1aa97e9-fdff-4b81-b468-0cb41be3e550",
"cn": "John Doe",
"sn": "Doe",
"mail": "[email protected]",
"givenName": "John"
}
"""- Add a new ldap entry with dn and with attributes
Given I add a ldap entry with dn "<dn>" and with attributes <attributes>
# Example:
Given I add a ldap entry with dn "uid=c1aa97e9-fdff-4b81-b468-0cb41be3e550,dc=company,dc=com" and with attributes "{ ... }"# π§ Context Utilities
Then I log "<value>"
Then I wait <seconds>s
Then I store "<key>" as "<value>" in context
Then I store as "<value>":
"""
My data
"""
# π HTTP Requests
Given I set http header "<key>" with "<value>"
When I request "<url>" with method "<method>"
When I request "<url>" with method "<method>" with query parameters
| key | value | type |
| userId | 123 | integer |
When I request "<url>" with method "<method>" with body:
"""
{ "name": "John" }
"""
# π₯ Response Assertions
Then I expect status code is <code>
Then I expect "<value>" is empty
Then I expect "<value>" is not empty
Then I expect "<value>" is "<expected>"
Then I expect "<value>" is not "<expected>"
Then I expect "<value>" is "<expected>" as "<type>"
Then I expect "<value>" to have length <number>
Then I expect "<value>" as "<type>" to have length <number>
Then I expect "<value>" contains "<expected>"
Then I expect "<value>" not contains "<expected>"
# π Resource Assertions
Then I expect one resource of "<value>" equals to "<expected>"
Then I expect one resource of "<value>" equals to "<expected>" as "<type>"
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>"
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>" as "<type>"
# π Manipulate localStorage
Given I set in localstorage field "<key>" with "<value>"
Then I expect localstorage field "<key>" is "<expected>"
Then I delete "<key>" in localstorage
Then I set localstorage field "<localStorageField>" to context field "<contextField>"
Then I set localstorage field "<localStorageField>" to context field "<contextField>" as "<type>"
# π URL Navigation & Assertions
Given I visit "<url>"
Given I reload to "<url>"
Then I expect current url is "<url>"
Then I expect current url contains "<url>"
Then I expect current url matches "<regex>"
Then I expect the current URL no longer is "<url>"
Then I expect the current URL no longer contains "<text>"
Then I expect the current URL no longer matches "<regex>"
# π±οΈ HTML Element Interactions
When I click on "<selector>"
When I force click on "<selector>"
When I double click on "<selector>"
When I scroll to "<position>" into "<selector>"
When I hover "<selector>" to make it visible
When I drag "<originSelector>" onto "<destinationSelector>"
When I drag "<selector>" of <x>,<y>
When I move "<selector>" of <x>,<y>
When I select "<option>" in "<selector>"
# ποΈ HTML Element Assertions
Then I expect the HTML element "<selector>" exists
Then I expect the HTML element "<selector>" not exists
Then I expect the HTML element "<selector>" to be visible
Then I expect the HTML element "<selector>" to be hidde
Then I expect the HTML element "<selector>" to be disabled
Then I expect the HTML element "<selector>" to be enabled
Then I expect the HTML element "<selector>" is checked
Then I expect the HTML element "<selector>" is not checked
Then I expect the HTML element "<selector>" width is <width>
Then I expect the HTML element "<selector>" height is <height>
Then I expect the HTML element "<selector>" to be at position <x>,<y>
Then I expect the HTML element "<selector>" to have attribute "<attribute>" with value "<value>"
Then I expect the HTML element "<selector>" contains "<text>"
Then I expect the HTML element "<selector>" not contains "<text>"
Then I expect the HTML element "<selector>" to have value "<value>"
Then I expect the HTML element "<selector>" appear <count> time(s) on screen
# βοΈ HTML Element Text Manipulation
Then I clear the text in the HTML element "<selector>"
Then I set the text "<text>" in the HTML element "<selector>"
# π Viewport Configuration
Given I set the viewport size to <width> px by <height> px
# β Kafka Messaging Steps
Given I setup kafka with clientId "<clientId>" and broker "<broker>"
Given I setup kafka producer
Given I setup kafka consumer with groupId "<groupId>"
Given I listen for Kafka messages on the topic "<topic>"
When I send a Kafka message on the topic "<topic>" with body "<body>"
When I send a Kafka message on the topic "<topic>" with body:
"""
{ "name": "John" }
"""
Then I expect <count> message(s) received on Kafka topic "<topic>"
Then I expect a message on Kafka topic "<topic>" equals to "<value>"
Then I expect a message on Kafka topic "<topic>" equals to "<value>" as "<type>"
Then I expect a message on Kafka topic "<topic>" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""
Then I expect a message on Kafka topic "<topic>" contains "<value>"
Then I expect a message on Kafka topic "<topic>" contains:
"""
{
"status": "shipped"
}
"""
Then I expect a message on Kafka topic "<topic>" matches regex "<regex>"
Then I log kafka messagesIf you need a step that doesn't exist yet, there are two options:
-
π¬ Open an issue: Create an issue describing the step you need, including:
- The Gherkin syntax youβd like to use
- A short example of the expected behavior
- Any relevant context or use case
-
π€ Contribute directly: If you're comfortable with JavaScript and Cypress, feel free to open a Pull Request. Please:
- Follow the existing step definitions style
- Add Gherkin usage and examples to the README
- Keep tests modular and consistent
π Contributions and feedback are welcome! This runner is designed to be extensible and team-friendly.