Skip to main content

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
}
Automatic Cleanup

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!

Alternative: Using @UsePlaywright

If 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:

AnnotationPurpose
@ExtendWith(SerenityJUnit5Extension.class)Serenity BDD reporting and step injection
@ExtendWith(SerenityPlaywrightExtension.class)Registers Playwright pages with Serenity for automatic screenshots
@UsePlaywrightManages the full Playwright lifecycle (Playwright, Browser, BrowserContext, Page)
Using @SerenityPlaywright

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 lifecycleManaged by ScreenplayManaged by @UsePlaywright
Browser instanceNew browser per actorReuses @UsePlaywright browser
TeardownCloses browser, context, pageOnly unregisters from Serenity
Tasks & QuestionsWork normallyWork normally
Screenshots & reportsCaptured automaticallyCaptured 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 @UsePlaywright tests 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​

  1. Readability: Tests read like specifications

    toby.attemptsTo(
    OpenTodoMvcApp.onTheHomePage(),
    AddATodoItem.called("Buy milk"),
    Complete.todoItem("Buy milk"),
    Ensure.that(TheRemainingCount.value()).isEqualTo(0)
    );
  2. Reusability: Tasks and Questions can be composed and reused

  3. Maintainability: Changes to UI are isolated to Target definitions

  4. 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​