Cucumber Integration with Playwright Screenplay
Cucumber provides a natural language syntax for writing behavior-driven tests. When combined with Serenity Playwright's Screenplay implementation, you get expressive BDD scenarios that drive reliable browser automation.
This guide shows you how to integrate Cucumber with Serenity Playwright using the Screenplay pattern.
Project Setup
Maven Dependencies
Add the following dependencies to your pom.xml:
<properties>
<serenity.version>5.1.1</serenity.version>
<playwright.version>1.58.0</playwright.version>
<cucumber.version>7.34.2</cucumber.version>
<junit.version>5.11.4</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Use BOMs for version management -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-bom</artifactId>
<version>${cucumber.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Serenity Screenplay with Playwright -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay-playwright</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Serenity Cucumber Integration -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-cucumber</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Cucumber (versions managed by cucumber-bom) -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit Platform Suite -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
<!-- Serenity Ensure for assertions -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-ensure</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Install Playwright Browsers
Before running tests, install the Playwright browsers:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
Project Structure
A typical project structure looks like this:
src/test/
├── java/
│ └── yourpackage/
│ ├── cucumber/
│ │ ├── CucumberTestSuite.java # Test runner
│ │ ├── PlaywrightHooks.java # Cucumber hooks
│ │ └── StepDefinitions.java # Step definitions
│ └── screenplay/
│ ├── tasks/ # Reusable tasks
│ │ ├── OpenTheApplication.java
│ │ └── AddATodoItem.java
│ ├── questions/ # Reusable questions
│ │ └── TheVisibleTodos.java
│ └── ui/ # Target definitions
│ └── TodoList.java
└── resources/
├── features/
│ └── managing_todos.feature # Feature files
└── serenity.properties # Configuration
Step 1: Create the Test Runner
The test runner configures Cucumber to work with Serenity:
package yourpackage.cucumber;
import org.junit.platform.suite.api.*;
import static io.cucumber.junit.platform.engine.Constants.*;
@Suite
@IncludeEngines("cucumber")
@SelectPackages("yourpackage.cucumber")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME,
value = "yourpackage.cucumber,net.serenitybdd.cucumber.actors")
@ConfigurationParameter(key = FEATURES_PROPERTY_NAME,
value = "src/test/resources/features")
public class CucumberTestSuite {
}
Always include net.serenitybdd.cucumber.actors in your glue path. This registers Serenity's StageDirector which automatically calls OnStage.drawTheCurtain() after each scenario to clean up browser resources.
Step 2: Create Playwright Hooks
The hooks class sets up the Playwright stage before each scenario:
package yourpackage.cucumber;
import io.cucumber.java.Before;
import io.cucumber.java.ParameterType;
import io.cucumber.java.Scenario;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.actors.OnStage;
import net.serenitybdd.screenplay.playwright.actors.PlaywrightCast;
public class PlaywrightHooks {
/**
* Set up the Playwright stage before each scenario.
* Order 0 ensures this runs first among all @Before hooks.
*/
@Before(order = 0)
public void setTheStage(Scenario scenario) {
OnStage.setTheStage(new PlaywrightCast());
}
/**
* Define the {actor} parameter type for Cucumber steps.
* This allows steps like "Given Toby is on the application"
* to automatically resolve "Toby" to an Actor instance.
*/
@ParameterType(".*")
public Actor actor(String actorName) {
return OnStage.theActorCalled(actorName);
}
}
Understanding the Hooks
@Before(order = 0): The order = 0 ensures the stage is set up before any other hooks run. This is important if you have other @Before hooks that depend on actors.
@ParameterType(".*"): This creates a custom parameter type that converts actor names in feature files to Actor instances. The pattern .* matches any string.
Automatic Cleanup: You don't need an @After hook for cleanup. The StageDirector (included via the glue path) automatically handles cleanup by calling OnStage.drawTheCurtain().
Step 3: Write Feature Files
Feature files describe behavior in Gherkin syntax:
# src/test/resources/features/managing_todos.feature
@playwright
Feature: Managing Todo Items
As a busy person
I want to manage my todo list
So that I can keep track of what I need to do
Background:
Given Toby is on the TodoMVC application
Scenario: Adding a single todo item
When Toby adds a todo item called "Buy milk"
Then the todo list should contain "Buy milk"
And the remaining item count should be 1
Scenario: Adding multiple todo items
When Toby adds the following todo items:
| Buy milk |
| Walk the dog |
| Do laundry |
Then the todo list should contain:
| Buy milk |
| Walk the dog |
| Do laundry |
And the remaining item count should be 3
Scenario: Completing a todo item
Given Toby has added the following todo items:
| Buy milk |
| Walk the dog |
When Toby completes the todo item "Walk the dog"
Then the remaining item count should be 1
Scenario: Filtering to show only active todos
Given Toby has added the following todo items:
| Buy milk |
| Walk the dog |
And Toby has completed the todo item "Walk the dog"
When Toby filters to show "Active" todos
Then the visible todo list should contain:
| Buy milk |
Step 4: Create Step Definitions
Step definitions bridge feature files to Screenplay tasks:
package yourpackage.cucumber;
import io.cucumber.java.en.*;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.actors.OnStage;
import net.serenitybdd.screenplay.ensure.Ensure;
import yourpackage.screenplay.tasks.*;
import yourpackage.screenplay.questions.*;
import java.util.List;
public class TodoStepDefinitions {
@Given("{actor} is on the TodoMVC application")
public void actorIsOnTheTodoMvcApplication(Actor actor) {
actor.attemptsTo(
OpenTheApplication.onTheTodoMvcHomePage()
);
}
@When("{actor} adds a todo item called {string}")
public void actorAddsTodoItemCalled(Actor actor, String todoItem) {
actor.attemptsTo(
AddATodoItem.called(todoItem)
);
}
@When("{actor} adds the following todo items:")
public void actorAddsTheFollowingTodoItems(Actor actor, List<String> todoItems) {
for (String item : todoItems) {
actor.attemptsTo(
AddATodoItem.called(item)
);
}
}
@Given("{actor} has added the following todo items:")
public void actorHasAddedTheFollowingTodoItems(Actor actor, List<String> todoItems) {
actorIsOnTheTodoMvcApplication(actor);
actorAddsTheFollowingTodoItems(actor, todoItems);
}
@When("{actor} completes the todo item {string}")
public void actorCompletesTheTodoItem(Actor actor, String todoItem) {
actor.attemptsTo(
Complete.todoItem(todoItem)
);
}
@Given("{actor} has completed the todo item {string}")
public void actorHasCompletedTheTodoItem(Actor actor, String todoItem) {
actorCompletesTheTodoItem(actor, todoItem);
}
@When("{actor} filters to show {string} todos")
public void actorFiltersToShow(Actor actor, String filterType) {
actor.attemptsTo(
FilterTodos.toShow(filterType)
);
}
@Then("the todo list should contain {string}")
public void theTodoListShouldContain(String expectedItem) {
Actor actor = OnStage.theActorInTheSpotlight();
actor.attemptsTo(
Ensure.that(TheVisibleTodos.displayed()).contains(expectedItem)
);
}
@Then("the todo list should contain:")
public void theTodoListShouldContainItems(List<String> expectedItems) {
Actor actor = OnStage.theActorInTheSpotlight();
actor.attemptsTo(
Ensure.that(TheVisibleTodos.displayed())
.containsExactlyElementsFrom(expectedItems)
);
}
@Then("the visible todo list should contain:")
public void theVisibleTodoListShouldContain(List<String> expectedItems) {
Actor actor = OnStage.theActorInTheSpotlight();
actor.attemptsTo(
Ensure.that(TheVisibleTodos.displayed())
.containsExactlyElementsFrom(expectedItems)
);
}
@Then("the remaining item count should be {int}")
public void theRemainingItemCountShouldBe(int expectedCount) {
Actor actor = OnStage.theActorInTheSpotlight();
actor.attemptsTo(
Ensure.that(TheRemainingCount.value()).isEqualTo(expectedCount)
);
}
}
Key Patterns in Step Definitions
Actor Parameter: The {actor} parameter automatically resolves to an Actor instance thanks to the @ParameterType definition in the hooks.
Background Steps: Steps like actorHasAddedTheFollowingTodoItems compose multiple actions, allowing Given steps to set up complex preconditions.
The Actor in the Spotlight: For Then steps that don't receive an actor parameter, use OnStage.theActorInTheSpotlight() to get the last actor who performed an action.
Data Tables: Cucumber data tables map directly to List<String> for single-column tables.
Step 5: Create Screenplay Tasks
Tasks encapsulate business-level actions:
package yourpackage.screenplay.tasks;
import net.serenitybdd.annotations.Step;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import net.serenitybdd.screenplay.playwright.interactions.Open;
public class OpenTheApplication implements Task {
private static final String TODO_MVC_URL = "https://todomvc.com/examples/react/dist/";
@Override
@Step("{0} opens the TodoMVC application")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Open.url(TODO_MVC_URL)
);
// Clear localStorage to ensure a clean slate
// (TodoMVC persists todos in localStorage)
var page = BrowseTheWebWithPlaywright.as(actor).getCurrentPage();
page.evaluate("() => localStorage.clear()");
page.reload();
}
public static OpenTheApplication onTheTodoMvcHomePage() {
return new OpenTheApplication();
}
}
package yourpackage.screenplay.tasks;
import net.serenitybdd.annotations.Step;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.*;
import yourpackage.screenplay.ui.TodoList;
public class AddATodoItem implements Task {
private final String itemName;
public AddATodoItem(String itemName) {
this.itemName = itemName;
}
@Override
@Step("{0} adds a todo item called '#itemName'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(itemName).into(TodoList.NEW_TODO_INPUT),
Press.keys("Enter")
);
}
public static AddATodoItem called(String itemName) {
return new AddATodoItem(itemName);
}
}
Step 6: Create Screenplay Questions
Questions query the application state:
package yourpackage.screenplay.questions;
import net.serenitybdd.annotations.Step;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import yourpackage.screenplay.ui.TodoList;
import java.util.Collection;
public class TheVisibleTodos implements Question<Collection<String>> {
@Override
@Step("{0} checks the visible todo items")
public Collection<String> answeredBy(Actor actor) {
var page = BrowseTheWebWithPlaywright.as(actor).getCurrentPage();
return page.locator(TodoList.TODO_ITEMS.asSelector())
.allTextContents();
}
public static TheVisibleTodos displayed() {
return new TheVisibleTodos();
}
}
Step 7: Define UI Targets
Targets define how to locate elements:
package yourpackage.screenplay.ui;
import net.serenitybdd.screenplay.playwright.Target;
public class TodoList {
public static final Target NEW_TODO_INPUT =
Target.the("new todo input").locatedBy(".new-todo");
public static final Target TODO_ITEMS =
Target.the("todo items").locatedBy(".todo-list li label");
public static final Target TODO_COUNT =
Target.the("todo count").locatedBy(".todo-count");
public static final Target TOGGLE_ALL =
Target.the("toggle all checkbox").locatedBy(".toggle-all");
}
Running the Tests
Run All Cucumber Tests
mvn clean verify
Run Tests with Specific Tags
mvn clean verify -Dcucumber.filter.tags="@playwright"
Run a Single Scenario
mvn clean verify -Dcucumber.filter.name="Adding a single todo item"
View Reports
After running tests, open the Serenity report:
target/site/serenity/index.html
Configuration
serenity.properties
# Browser settings
playwright.browsertype=chromium
playwright.headless=true
# Screenshot settings
serenity.take.screenshots=FOR_FAILURES
# Report settings
serenity.project.name=My TodoMVC Tests
cucumber.properties
# Cucumber settings
cucumber.publish.quiet=true
Advanced Configuration
Custom Browser Configuration
Configure PlaywrightCast with custom options:
@Before(order = 0)
public void setTheStage(Scenario scenario) {
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions()
.setHeadless(false)
.setSlowMo(100); // Slow down for debugging
OnStage.setTheStage(
new PlaywrightCast()
.withLaunchOptions(options)
.withBrowserType("firefox")
);
}
Multiple Actors
Scenarios can involve multiple actors:
Scenario: Collaborative editing
Given Alice has added a todo item called "Shared task"
When Bob views the todo list
Then Bob should see "Shared task" in the list
@When("{actor} views the todo list")
public void actorViewsTheTodoList(Actor actor) {
actor.attemptsTo(
OpenTheApplication.onTheTodoMvcHomePage()
);
}
Each actor gets their own browser instance automatically through PlaywrightCast.
Troubleshooting
State Leakage Between Scenarios
If tests interfere with each other:
- Verify
net.serenitybdd.cucumber.actorsis in your glue path - Clear application state (like localStorage) in your navigation task
- Ensure
@Before(order = 0)is set on your stage setup hook
Element Not Found
If elements aren't being found:
- Use Playwright's built-in auto-waiting (it's enabled by default)
- Check your selectors using browser DevTools
- Consider using Playwright's more robust selectors like
text=,role=, etc.
Screenshots Not Captured
Set screenshot mode in serenity.properties:
serenity.take.screenshots=FOR_EACH_ACTION
Example Repository
A complete working example is available in the serenity-playwright-todomvc-demo repository.
See Also
- Screenplay Pattern with Playwright - Core Screenplay concepts
- Tutorial: Screenplay with TodoMVC - Building a test suite from scratch
- Cucumber Configuration Reference - All Cucumber options
- Parallel Execution - Running tests in parallel