Tutorial: Building a Screenplay Test Suite
This tutorial walks through building a Serenity Playwright test suite using the Screenplay Pattern for the TodoMVC application. We'll build it iteratively, starting with the fundamentals and progressively adding more sophisticated patterns.
What We're Buildingβ
By the end of this tutorial, you'll have a complete test suite with:
- Targets for all UI elements
- Tasks for user actions (add, complete, delete, filter todos)
- Questions to query application state
- Tests using
Ensure.that()assertions
Project Setupβ
Create a Maven project with these dependencies:
<properties>
<serenity.version>5.3.1</serenity.version>
<playwright.version>1.58.0</playwright.version>
</properties>
<dependencies>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay-playwright</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-ensure</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
Part 1: Our First Testβ
Let's start with the simplest possible test - adding a single todo item.
Step 1: Create the Base Test Classβ
First, create a base class that sets up our actor with Playwright abilities:
package todomvc.screenplay;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import org.junit.jupiter.api.BeforeEach;
public abstract class ScreenplayPlaywrightTest {
protected Actor toby;
@BeforeEach
void setUpPlaywright() {
toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration());
}
// No @AfterEach needed - BrowseTheWebWithPlaywright automatically
// cleans up browser resources when the test finishes
}
The BrowseTheWebWithPlaywright ability subscribes to Serenity's test lifecycle events and automatically closes the browser, context, and page when each test completes. You don't need to write any teardown code!
@UsePlaywrightIf you prefer to use Playwright's @UsePlaywright annotation for browser lifecycle management, you can wrap the injected Page instead:
import com.microsoft.playwright.Page;
import com.microsoft.playwright.junit.UsePlaywright;
import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
@ExtendWith(SerenityPlaywrightExtension.class)
@UsePlaywright
public abstract class ScreenplayPlaywrightTest {
protected Actor toby;
@BeforeEach
void setUpPlaywright(Page page) {
toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.withPage(page));
}
}
The SerenityPlaywrightExtension registers Playwright pages with Serenity for automatic screenshot capture. This shares a single browser instance between @UsePlaywright and Screenplay, instead of creating two separate browsers. See Using @UsePlaywright with Screenplay at the end of this tutorial for a full walkthrough.
Step 2: Define UI Targetsβ
Create a class to hold all UI element locators:
package todomvc.screenplay.ui;
import net.serenitybdd.screenplay.playwright.Target;
public class TodoList {
public static final Target NEW_TODO_INPUT =
Target.the("new todo input")
.locatedBy("[placeholder='What needs to be done?']");
public static final Target TODO_ITEMS =
Target.the("todo items")
.locatedBy(".todo-list li");
public static final Target TODO_ITEM_LABELS =
Target.the("todo item labels")
.locatedBy(".todo-list li label");
}
Step 3: Create the First Taskβ
Create a task to open the TodoMVC application:
package todomvc.screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.Open;
import net.serenitybdd.annotations.Step;
public class OpenTodoMvcApp implements Task {
private static final String 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(URL));
}
public static OpenTodoMvcApp onTheHomePage() {
return new OpenTodoMvcApp();
}
}
Step 4: Create the Add Todo Taskβ
package todomvc.screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.Enter;
import net.serenitybdd.screenplay.playwright.interactions.Press;
import net.serenitybdd.annotations.Step;
import todomvc.screenplay.ui.TodoList;
public class AddATodoItem implements Task {
private final String todoItem;
public AddATodoItem(String todoItem) {
this.todoItem = todoItem;
}
@Override
@Step("{0} adds a todo item called '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(todoItem).into(TodoList.NEW_TODO_INPUT),
Press.keys("Enter")
);
}
public static AddATodoItem called(String todoItem) {
return new AddATodoItem(todoItem);
}
}
Step 5: Create a Questionβ
Create a question to count visible todos:
package todomvc.screenplay.questions;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import todomvc.screenplay.ui.TodoList;
public class TheVisibleTodos {
public static Question<Integer> count() {
return Question.about("visible todo count").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.TODO_ITEMS.asSelector())
.count()
);
}
}
Step 6: Write the Testβ
package todomvc.screenplay;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.screenplay.ensure.Ensure;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import todomvc.screenplay.questions.TheVisibleTodos;
import todomvc.screenplay.tasks.AddATodoItem;
import todomvc.screenplay.tasks.OpenTodoMvcApp;
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When adding todos")
class WhenAddingTodosTest extends ScreenplayPlaywrightTest {
@Test
@DisplayName("should add a single todo item")
void shouldAddSingleTodoItem() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
Ensure.that(TheVisibleTodos.count()).isEqualTo(1)
);
}
}
Run it with mvn clean verify - you should see a passing test with a Serenity report.
Part 2: Expanding Our Test Suiteβ
Now let's add more capabilities.
Adding More Targetsβ
Expand the TodoList class with more locators:
public class TodoList {
// Input elements
public static final Target NEW_TODO_INPUT =
Target.the("new todo input")
.locatedBy("[placeholder='What needs to be done?']");
// Todo items
public static final Target TODO_ITEMS =
Target.the("todo items")
.locatedBy(".todo-list li");
public static final Target TODO_ITEM_LABELS =
Target.the("todo item labels")
.locatedBy(".todo-list li label");
// Dynamic targets for specific items
public static Target todoItemCalled(String text) {
return Target.the("todo '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "')");
}
public static Target checkboxFor(String text) {
return Target.the("checkbox for '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "') .toggle");
}
public static Target deleteButtonFor(String text) {
return Target.the("delete button for '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "') .destroy");
}
// Footer elements
public static final Target TODO_COUNT =
Target.the("todo count")
.locatedBy(".todo-count");
public static final Target CLEAR_COMPLETED_BUTTON =
Target.the("clear completed button")
.locatedBy(".clear-completed");
// Filters
public static final Target ALL_FILTER =
Target.the("All filter")
.locatedBy(".filters a:has-text('All')");
public static final Target ACTIVE_FILTER =
Target.the("Active filter")
.locatedBy(".filters a:has-text('Active')");
public static final Target COMPLETED_FILTER =
Target.the("Completed filter")
.locatedBy(".filters a:has-text('Completed')");
public static final Target SELECTED_FILTER =
Target.the("selected filter")
.locatedBy(".filters a.selected");
}
Complete Taskβ
package todomvc.screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.Click;
import net.serenitybdd.annotations.Step;
import todomvc.screenplay.ui.TodoList;
public class Complete implements Task {
private final String todoItem;
public Complete(String todoItem) {
this.todoItem = todoItem;
}
@Override
@Step("{0} completes the todo item '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Click.on(TodoList.checkboxFor(todoItem))
);
}
public static Complete todoItem(String todoItem) {
return new Complete(todoItem);
}
}
Delete Taskβ
package todomvc.screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.Click;
import net.serenitybdd.screenplay.playwright.interactions.Hover;
import net.serenitybdd.annotations.Step;
import todomvc.screenplay.ui.TodoList;
public class Delete implements Task {
private final String todoItem;
public Delete(String todoItem) {
this.todoItem = todoItem;
}
@Override
@Step("{0} deletes the todo item '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Hover.over(TodoList.todoItemCalled(todoItem)),
Click.on(TodoList.deleteButtonFor(todoItem))
);
}
public static Delete theTodoItem(String todoItem) {
return new Delete(todoItem);
}
}
Filter Tasksβ
package todomvc.screenplay.tasks;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.playwright.interactions.Click;
import todomvc.screenplay.ui.TodoList;
public class FilterTodos {
public static Task toShowAll() {
return Task.where("{0} filters to show all todos",
Click.on(TodoList.ALL_FILTER)
);
}
public static Task toShowActive() {
return Task.where("{0} filters to show active todos",
Click.on(TodoList.ACTIVE_FILTER)
);
}
public static Task toShowCompleted() {
return Task.where("{0} filters to show completed todos",
Click.on(TodoList.COMPLETED_FILTER)
);
}
}
More Questionsβ
// TheVisibleTodos.java - expanded
package todomvc.screenplay.questions;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import todomvc.screenplay.ui.TodoList;
import java.util.Collection;
public class TheVisibleTodos {
public static Question<Collection<String>> displayed() {
return Question.about("the visible todos").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.TODO_ITEM_LABELS.asSelector())
.allTextContents()
);
}
public static Question<Integer> count() {
return Question.about("visible todo count").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.TODO_ITEMS.asSelector())
.count()
);
}
}
// TheRemainingCount.java
package todomvc.screenplay.questions;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import todomvc.screenplay.ui.TodoList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TheRemainingCount {
private static final Pattern COUNT_PATTERN = Pattern.compile("(\\d+)");
public static Question<Integer> value() {
return Question.about("the remaining count").answeredBy(actor -> {
String text = BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.TODO_COUNT.asSelector())
.textContent();
Matcher matcher = COUNT_PATTERN.matcher(text);
return matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
});
}
}
// TheCurrentFilter.java
package todomvc.screenplay.questions;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import todomvc.screenplay.ui.TodoList;
public class TheCurrentFilter {
public static Question<String> selected() {
return Question.about("the current filter").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.SELECTED_FILTER.asSelector())
.textContent()
);
}
}
// TheTodoItem.java - fluent question builder
package todomvc.screenplay.questions;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import todomvc.screenplay.ui.TodoList;
public class TheTodoItem {
private final String todoItem;
public TheTodoItem(String todoItem) {
this.todoItem = todoItem;
}
public static TheTodoItem called(String todoItem) {
return new TheTodoItem(todoItem);
}
public Question<Boolean> exists() {
String item = todoItem;
return Question.about("whether '" + item + "' exists").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.todoItemCalled(item).asSelector())
.count() > 0
);
}
public Question<Boolean> isCompleted() {
String item = todoItem;
return Question.about("whether '" + item + "' is completed").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(TodoList.todoItemCalled(item).asSelector())
.getAttribute("class")
.contains("completed")
);
}
}
Part 3: Complete Test Suiteβ
Now we have everything to write a comprehensive test suite:
Adding Todos Testsβ
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When adding todos (Screenplay)")
class WhenAddingTodosScreenplayTest extends ScreenplayPlaywrightTest {
@Test
@DisplayName("should add a single todo item")
void shouldAddSingleTodoItem() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
Ensure.that(TheVisibleTodos.count()).isEqualTo(1),
Ensure.that(TheTodoItem.called("Buy milk").exists()).isTrue()
);
}
@Test
@DisplayName("should add multiple todo items")
void shouldAddMultipleTodoItems() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
AddATodoItem.called("Walk the dog"),
AddATodoItem.called("Do laundry"),
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Walk the dog", "Do laundry")
);
}
@Test
@DisplayName("should update remaining count when adding todos")
void shouldUpdateRemainingCountWhenAddingTodos() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Task 1"),
Ensure.that(TheRemainingCount.value()).isEqualTo(1),
AddATodoItem.called("Task 2"),
Ensure.that(TheRemainingCount.value()).isEqualTo(2),
AddATodoItem.called("Task 3"),
Ensure.that(TheRemainingCount.value()).isEqualTo(3)
);
}
}
Completing Todos Testsβ
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When completing todos (Screenplay)")
class WhenCompletingTodosScreenplayTest extends ScreenplayPlaywrightTest {
@BeforeEach
void setupTodos() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
AddATodoItem.called("Walk the dog"),
AddATodoItem.called("Do laundry")
);
}
@Test
@DisplayName("should mark a todo as completed")
void shouldMarkTodoAsCompleted() {
toby.attemptsTo(
Complete.todoItem("Buy milk"),
Ensure.that(TheTodoItem.called("Buy milk").isCompleted()).isTrue(),
Ensure.that(TheRemainingCount.value()).isEqualTo(2)
);
}
@Test
@DisplayName("should clear completed todos")
void shouldClearCompletedTodos() {
toby.attemptsTo(
Complete.todoItem("Buy milk"),
Complete.todoItem("Walk the dog"),
Click.on(TodoList.CLEAR_COMPLETED_BUTTON),
Ensure.that(TheVisibleTodos.displayed()).containsExactly("Do laundry"),
Ensure.that(TheVisibleTodos.count()).isEqualTo(1)
);
}
}
Filtering Testsβ
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When filtering todos (Screenplay)")
class WhenFilteringTodosScreenplayTest extends ScreenplayPlaywrightTest {
@BeforeEach
void setupTodos() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
AddATodoItem.called("Walk the dog"),
AddATodoItem.called("Do laundry"),
Complete.todoItem("Walk the dog")
);
}
@Test
@DisplayName("should show all todos by default")
void shouldShowAllTodosByDefault() {
toby.attemptsTo(
Ensure.that(TheCurrentFilter.selected()).isEqualTo("All"),
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Walk the dog", "Do laundry")
);
}
@Test
@DisplayName("should filter to show only active todos")
void shouldFilterToShowOnlyActiveTodos() {
toby.attemptsTo(
FilterTodos.toShowActive(),
Ensure.that(TheCurrentFilter.selected()).isEqualTo("Active"),
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Do laundry")
);
}
@Test
@DisplayName("should filter to show only completed todos")
void shouldFilterToShowOnlyCompletedTodos() {
toby.attemptsTo(
FilterTodos.toShowCompleted(),
Ensure.that(TheCurrentFilter.selected()).isEqualTo("Completed"),
Ensure.that(TheVisibleTodos.displayed()).containsExactly("Walk the dog")
);
}
@Test
@DisplayName("should switch between filters")
void shouldSwitchBetweenFilters() {
toby.attemptsTo(
FilterTodos.toShowActive(),
Ensure.that(TheVisibleTodos.count()).isEqualTo(2),
FilterTodos.toShowCompleted(),
Ensure.that(TheVisibleTodos.count()).isEqualTo(1),
FilterTodos.toShowAll(),
Ensure.that(TheVisibleTodos.count()).isEqualTo(3)
);
}
}
Deleting Testsβ
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When deleting todos (Screenplay)")
class WhenDeletingTodosScreenplayTest extends ScreenplayPlaywrightTest {
@BeforeEach
void setupTodos() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
AddATodoItem.called("Walk the dog"),
AddATodoItem.called("Do laundry")
);
}
@Test
@DisplayName("should delete a todo item")
void shouldDeleteTodoItem() {
toby.attemptsTo(
Delete.theTodoItem("Walk the dog"),
Ensure.that(TheTodoItem.called("Walk the dog").exists()).isFalse(),
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Do laundry")
);
}
@Test
@DisplayName("should update the remaining count after deleting")
void shouldUpdateRemainingCountAfterDeleting() {
toby.attemptsTo(
Ensure.that(TheRemainingCount.value()).isEqualTo(3),
Delete.theTodoItem("Buy milk"),
Ensure.that(TheRemainingCount.value()).isEqualTo(2)
);
}
}
Using @UsePlaywright with Screenplayβ
Throughout this tutorial, we used BrowseTheWebWithPlaywright.usingTheDefaultConfiguration() β which creates and manages its own Playwright browser. But if your project already uses Playwright's @UsePlaywright annotation (for example, to share browser configuration or integrate with Playwright's built-in test fixtures), you can adapt the Screenplay tests to reuse that browser session.
Updating the Base Test Classβ
Replace usingTheDefaultConfiguration() with withPage(page), and add the @UsePlaywright and SerenityPlaywrightExtension annotations:
package todomvc.screenplay;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.junit.UsePlaywright;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(SerenityJUnit5Extension.class)
@ExtendWith(SerenityPlaywrightExtension.class)
@UsePlaywright
public abstract class ScreenplayPlaywrightTest {
protected Actor toby;
@BeforeEach
void setUpPlaywright(Page page) {
toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.withPage(page));
}
}
Three annotations work together:
| Annotation | Purpose |
|---|---|
@ExtendWith(SerenityJUnit5Extension.class) | Serenity BDD reporting and step injection |
@ExtendWith(SerenityPlaywrightExtension.class) | Registers Playwright pages with Serenity for automatic screenshots |
@UsePlaywright | Manages the full Playwright lifecycle (Playwright, Browser, BrowserContext, Page) |
You can replace both @ExtendWith annotations with the shorthand @SerenityPlaywright:
@SerenityPlaywright
@UsePlaywright
public abstract class ScreenplayPlaywrightTest {
// ...
}
That's it. All the tasks, questions, targets, and test classes from this tutorial work unchanged β the only difference is how the actor's ability is created.
What Changesβ
usingTheDefaultConfiguration() | withPage(page) | |
|---|---|---|
| Browser lifecycle | Managed by Screenplay | Managed by @UsePlaywright |
| Browser instance | New browser per actor | Reuses @UsePlaywright browser |
| Teardown | Closes browser, context, page | Only unregisters from Serenity |
| Tasks & Questions | Work normally | Work normally |
| Screenshots & reports | Captured automatically | Captured automatically |
Custom Playwright Optionsβ
With @UsePlaywright, you can customize browser options by implementing OptionsFactory:
import com.microsoft.playwright.*;
import com.microsoft.playwright.junit.Options;
import com.microsoft.playwright.junit.OptionsFactory;
public class CustomOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setLaunchOptions(new BrowserType.LaunchOptions().setHeadless(true))
.setContextOptions(new Browser.NewContextOptions()
.setViewportSize(1920, 1080)
.setLocale("en-US"));
}
}
Then reference it in your tests:
@ExtendWith(SerenityJUnit5Extension.class)
@ExtendWith(SerenityPlaywrightExtension.class)
@UsePlaywright(CustomOptions.class)
public abstract class ScreenplayPlaywrightTest {
// ...same as above
}
When to Use Which Approachβ
usingTheDefaultConfiguration()β simplest setup; ideal when Screenplay is your only test framework and you don't need Playwright's JUnit integration features.withPage(page)β use when you already have@UsePlaywrighttests and want to add Screenplay actors to them, or when you need Playwright's built-in fixtures (tracing, video recording, custom options) alongside Screenplay.
Key Takeawaysβ
The Screenplay Pattern Benefitsβ
-
Readability: Tests read like specifications
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
Complete.todoItem("Buy milk"),
Ensure.that(TheRemainingCount.value()).isEqualTo(0)
); -
Reusability: Tasks and Questions can be composed and reused
-
Maintainability: Changes to UI are isolated to Target definitions
-
Expressiveness: Business intent is clear in every test
Project Structureβ
src/test/java/
βββ todomvc/screenplay/
β βββ ScreenplayPlaywrightTest.java # Base test class
β βββ WhenAddingTodosTest.java # Test class
β βββ WhenCompletingTodosTest.java # Test class
β βββ WhenFilteringTodosTest.java # Test class
β βββ WhenDeletingTodosTest.java # Test class
β βββ ui/
β β βββ TodoList.java # UI Targets
β βββ tasks/
β β βββ OpenTodoMvcApp.java
β β βββ AddATodoItem.java
β β βββ Complete.java
β β βββ Delete.java
β β βββ FilterTodos.java
β βββ questions/
β βββ TheVisibleTodos.java
β βββ TheRemainingCount.java
β βββ TheCurrentFilter.java
β βββ TheTodoItem.java
Next Stepsβ
- Learn about advanced Screenplay features including network interception and multiple tabs
- Explore Playwright-specific assertions with auto-retry
- See Best Practices for production-ready tests