Skip to main content

Web Testing with Serenity Screenplay and Playwright

Introduction​

Playwright is a modern browser automation library that provides cross-browser testing capabilities with excellent support for modern web applications. The serenity-screenplay-playwright module brings the power of Playwright to Serenity BDD's Screenplay pattern, offering an alternative to the traditional WebDriver integration.

Playwright offers several advantages over WebDriver:

  • Auto-waiting: Automatically waits for elements to be actionable before performing actions
  • Cross-browser: Native support for Chromium, Firefox, and WebKit
  • Network interception: Built-in support for mocking and intercepting network requests
  • Tracing: Record detailed traces for debugging test failures
  • Device emulation: Easy mobile and tablet device emulation
  • Modern architecture: Event-driven design with better reliability

Getting Started​

Maven Dependency​

Add the following dependency to your project:

<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay-playwright</artifactId>
<version>${serenity.version}</version>
</dependency>

The BrowseTheWebWithPlaywright Ability​

To use Playwright with Screenplay, actors need the BrowseTheWebWithPlaywright ability. The recommended approach is to use Playwright's @UsePlaywright annotation, which manages the browser lifecycle automatically:

import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import com.microsoft.playwright.*;

@ExtendWith(SerenityJUnit5Extension.class)
@ExtendWith(SerenityPlaywrightExtension.class)
@UsePlaywright(ChromeHeadlessOptions.class)
class MyScreenplayTest {

Actor alice;

@BeforeEach
void setUp(Page page) {
alice = Actor.named("Alice");
alice.can(BrowseTheWebWithPlaywright.withPage(page));
}
}

The withPage(Page) factory method wraps a @UsePlaywright-managed page. Screenplay reuses the existing browser β€” no duplicate browser instance is created. On teardown, Screenplay only unregisters the page; it does not close the Page, BrowserContext, Browser, or Playwright instance.

@SerenityPlaywright shorthand

You can use the @SerenityPlaywright meta-annotation as a shorthand for both @ExtendWith(SerenityJUnit5Extension.class) and @ExtendWith(SerenityPlaywrightExtension.class).

Alternative: Manual Lifecycle Management​

If you need full control over the browser lifecycle, you can manage it manually:

Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();

Actor alice = Actor.named("Alice");
alice.can(BrowseTheWebWithPlaywright.using(browser));

Configuration​

When using @UsePlaywright, configure browser options with an OptionsFactory:

public class ChromeHeadlessOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setHeadless(true)
.setLaunchOptions(
new BrowserType.LaunchOptions()
.setArgs(Arrays.asList(
"--no-sandbox",
"--disable-extensions",
"--disable-gpu"))
);
}
}

For manual lifecycle management, configure programmatically:

BrowserType.LaunchOptions options = new BrowserType.LaunchOptions()
.setHeadless(false)
.setSlowMo(100);

Browser browser = playwright.chromium().launch(options);

Opening a URL​

Opening a URL directly​

In Screenplay, you open a new page using the Navigate interaction class:

alice.attemptsTo(Navigate.to("https://todomvc.com/examples/react/"));

Opening the base URL​

If you have configured a base URL, you can navigate to it:

alice.attemptsTo(Navigate.toTheBaseUrl());

Locating Elements on a Page​

The Target Class​

The Target class in Playwright integration uses Playwright's powerful selector engine. Unlike WebDriver, Playwright provides multiple built-in selector strategies.

Target SUBMIT_BUTTON = Target.the("Submit button").locatedBy("#submit-btn");

alice.attemptsTo(Click.on(SUBMIT_BUTTON));

Playwright Selectors​

Playwright supports a rich set of selectors:

// CSS selector
Target.the("Login button").locatedBy("#login-btn")

// Text selector
Target.the("Submit button").locatedBy("text=Submit")

// Role selector (ARIA)
Target.the("Submit button").locatedBy("role=button[name='Submit']")

// XPath selector
Target.the("Email field").locatedBy("xpath=//input[@type='email']")

// Combining selectors
Target.the("Form submit").locatedBy("form >> button[type='submit']")

Dynamic Targets​

You can use parameterized targets for dynamic element location:

Target MENU_ITEM = Target.the("{0} menu item").locatedBy("text={0}");

alice.attemptsTo(Click.on(MENU_ITEM.of("Settings")));

UI Element Factories​

Serenity Playwright provides convenient factory classes for locating common UI elements using Playwright's powerful selector syntax.

Button​

Locate buttons using various strategies:

import net.serenitybdd.screenplay.playwright.ui.Button;

// By visible text (case-insensitive, uses role selector)
alice.attemptsTo(Click.on(Button.withText("Submit")));

// By name or ID attribute
alice.attemptsTo(Click.on(Button.withNameOrId("submit-btn")));

// By aria-label
alice.attemptsTo(Click.on(Button.withAriaLabel("Close dialog")));

// Containing specific text
alice.attemptsTo(Click.on(Button.containingText("Add to")));

// Custom locator
alice.attemptsTo(Click.on(Button.locatedBy("[data-testid='primary-action']")));

InputField​

Locate input fields:

import net.serenitybdd.screenplay.playwright.ui.InputField;

// By name or ID
alice.attemptsTo(Enter.theValue("john@example.com").into(InputField.withNameOrId("email")));

// By placeholder text
alice.attemptsTo(Enter.theValue("Search...").into(InputField.withPlaceholder("Search products")));

// By associated label text
alice.attemptsTo(Enter.theValue("password123").into(InputField.withLabel("Password")));

// By aria-label
alice.attemptsTo(Enter.theValue("John").into(InputField.withAriaLabel("First name")));

Locate anchor elements:

import net.serenitybdd.screenplay.playwright.ui.Link;

// By exact text
alice.attemptsTo(Click.on(Link.withText("Learn more")));

// Containing text
alice.attemptsTo(Click.on(Link.containingText("documentation")));

// By title attribute
alice.attemptsTo(Click.on(Link.withTitle("View user profile")));

Checkbox​

Locate checkbox inputs:

import net.serenitybdd.screenplay.playwright.ui.Checkbox;

// By label text
alice.attemptsTo(Click.on(Checkbox.withLabel("Accept terms")));

// By name or ID
alice.attemptsTo(Click.on(Checkbox.withNameOrId("newsletter")));

// By value attribute
alice.attemptsTo(Click.on(Checkbox.withValue("premium")));

RadioButton​

Locate radio button inputs:

import net.serenitybdd.screenplay.playwright.ui.RadioButton;

// By label text
alice.attemptsTo(Click.on(RadioButton.withLabel("Express shipping")));

// By value attribute
alice.attemptsTo(Click.on(RadioButton.withValue("express")));

Locate select elements:

import net.serenitybdd.screenplay.playwright.ui.Dropdown;

// By label text
alice.attemptsTo(
SelectFromOptions.byVisibleText("Canada").from(Dropdown.withLabel("Country"))
);

// By name or ID
alice.attemptsTo(
SelectFromOptions.byValue("us").from(Dropdown.withNameOrId("country"))
);

Label​

Locate label elements:

import net.serenitybdd.screenplay.playwright.ui.Label;

// By text content
String labelText = alice.asksFor(Text.of(Label.withText("Email")));

// For a specific field ID
Target emailLabel = Label.forFieldId("email-input");

Image​

Locate image elements:

import net.serenitybdd.screenplay.playwright.ui.Image;

// By alt text
alice.attemptsTo(Click.on(Image.withAltText("Product thumbnail")));

// By source URL
alice.attemptsTo(Click.on(Image.withSrc("/images/logo.png")));

// By partial source URL
alice.attemptsTo(Click.on(Image.withSrcContaining("product-123")));

Interacting with Elements​

Core Interactions​

The following interaction classes are available in the net.serenitybdd.screenplay.playwright.interactions package:

InteractionPurposeExample
ClickClick on an elementactor.attemptsTo(Click.on("#button"))
DoubleClickDouble-click on an elementactor.attemptsTo(DoubleClick.on("#item"))
RightClickRight-click (context menu)actor.attemptsTo(RightClick.on("#menu"))
EnterType into an input fieldactor.attemptsTo(Enter.theValue("text").into("#field"))
ClearClear an input fieldactor.attemptsTo(Clear.field("#field"))
HoverHover over an elementactor.attemptsTo(Hover.over("#menu"))
PressPress keyboard keysactor.attemptsTo(Press.key("Enter"))
CheckCheck a checkboxactor.attemptsTo(Check.checkbox("#agree"))
UncheckUncheck a checkboxactor.attemptsTo(Uncheck.checkbox("#agree"))
FocusFocus on an elementactor.attemptsTo(Focus.on("#input"))
NavigateNavigate to a URLactor.attemptsTo(Navigate.to("https://..."))
UploadUpload a fileactor.attemptsTo(Upload.file(path).to("#upload"))

Click​

Click on an element. Playwright automatically waits for the element to be actionable:

alice.attemptsTo(Click.on("#submit-button"));
alice.attemptsTo(Click.on(SUBMIT_BUTTON));

Double-Click​

Double-click on an element:

alice.attemptsTo(DoubleClick.on("#item"));

Right-Click​

Right-click to open context menus:

alice.attemptsTo(RightClick.on("#file-item"));

Enter Text​

Type values into input fields:

alice.attemptsTo(Enter.theValue("john@example.com").into("#email"));

You can also clear the field first:

alice.attemptsTo(
Clear.field("#email"),
Enter.theValue("new-email@example.com").into("#email")
);

Keyboard Interactions​

Press keyboard keys:

// Single key
alice.attemptsTo(Press.key("Enter"));

// Key combinations
alice.attemptsTo(Press.key("Control+a"));

// Multiple keys
alice.attemptsTo(Press.keys("Tab", "Tab", "Enter"));

Hover​

Hover over elements to trigger hover states:

alice.attemptsTo(Hover.over("#dropdown-menu"));

Check and Uncheck​

Work with checkboxes:

alice.attemptsTo(Check.checkbox("#newsletter"));
alice.attemptsTo(Uncheck.checkbox("#marketing-emails"));

Focus​

Focus on an element:

alice.attemptsTo(Focus.on("#search-input"));

Selecting from Dropdowns​

Select options from dropdown menus:

// By visible text
alice.attemptsTo(SelectFromOptions.byVisibleText("Red").from("#color"));

// By value attribute
alice.attemptsTo(SelectFromOptions.byValue("red").from("#color"));

// By index
alice.attemptsTo(SelectFromOptions.byIndex(2).from("#color"));

// Multiple values (for multi-select)
alice.attemptsTo(SelectFromOptions.byValue("red", "blue", "green").from("#colors"));

Deselecting from Dropdowns​

For multi-select dropdowns, you can deselect options:

import net.serenitybdd.screenplay.playwright.interactions.DeselectFromOptions;

// Deselect by value
alice.attemptsTo(DeselectFromOptions.byValue("red").from("#colors"));

// Deselect by visible text
alice.attemptsTo(DeselectFromOptions.byVisibleText("Red").from("#colors"));

// Deselect by index
alice.attemptsTo(DeselectFromOptions.byIndex(0).from("#colors"));

// Deselect all
alice.attemptsTo(DeselectFromOptions.all().from("#colors"));

Scrolling​

Comprehensive scrolling capabilities:

import net.serenitybdd.screenplay.playwright.interactions.Scroll;

// Scroll to an element
alice.attemptsTo(Scroll.to("#terms-and-conditions"));

// Scroll with alignment
alice.attemptsTo(Scroll.to("#section").andAlignToTop());
alice.attemptsTo(Scroll.to("#section").andAlignToCenter());
alice.attemptsTo(Scroll.to("#section").andAlignToBottom());

// Page-level scrolling
alice.attemptsTo(Scroll.toTop());
alice.attemptsTo(Scroll.toBottom());

// Scroll by specific amount (deltaX, deltaY)
alice.attemptsTo(Scroll.by(0, 500));

// Scroll to specific position
alice.attemptsTo(Scroll.toPosition(0, 1000));

Drag and Drop​

Drag elements from one location to another:

import net.serenitybdd.screenplay.playwright.interactions.Drag;

// Basic drag and drop
alice.attemptsTo(Drag.from("#source").to("#target"));

// Alternative fluent syntax
alice.attemptsTo(Drag.the("#draggable").onto("#droppable"));

// With Targets
alice.attemptsTo(Drag.from(SOURCE_ELEMENT).to(TARGET_LOCATION));

File Uploads​

Upload files:

Path fileToUpload = Paths.get("path/to/file.pdf");
alice.attemptsTo(Upload.file(fileToUpload).to("#file-input"));

JavaScript Execution​

Execute JavaScript in the page context:

alice.attemptsTo(
Evaluate.javascript("window.scrollTo(0, document.body.scrollHeight)")
);

// With return value
Object result = alice.asksFor(
Evaluate.javascript("return document.title")
);

Waiting​

Wait for elements or conditions:

// Wait for element to be visible
alice.attemptsTo(WaitUntil.the("#loading").isNotVisible());

// Wait for element to be hidden
alice.attemptsTo(WaitUntil.the("#spinner").isHidden());

// Wait with timeout
alice.attemptsTo(
WaitUntil.the("#content").isVisible()
.forNoMoreThan(Duration.ofSeconds(10))
);

Querying the Web Page​

Bundled Questions​

Serenity Playwright provides Question classes for querying page state:

QuestionPurposeExample
TextGet element textactor.asksFor(Text.of("#title"))
ValueGet input valueactor.asksFor(Value.of("#email"))
AttributeGet attribute valueactor.asksFor(Attribute.of("#link").named("href"))
PresenceCheck if element existsactor.asksFor(Presence.of("#modal"))
AbsenceCheck if element is absentactor.asksFor(Absence.of("#error"))
VisibilityCheck if element is visibleactor.asksFor(Visibility.of("#popup"))
EnabledCheck if element is enabledactor.asksFor(Enabled.of("#submit"))
SelectedStatusCheck if checkbox is selectedactor.asksFor(SelectedStatus.of("#agree"))
CSSValueGet CSS propertyactor.asksFor(CSSValue.of("#box").named("color"))
CurrentUrlGet current page URLactor.asksFor(CurrentUrl.ofThePage())
PageTitleGet page titleactor.asksFor(PageTitle.ofThePage())

Text​

Get the text content of an element:

String heading = alice.asksFor(Text.of("#main-heading"));

// Multiple elements
List<String> items = alice.asksFor(Text.ofEach(".list-item"));

Presence and Absence​

Check whether elements exist on the page:

// Check if present
boolean isPresent = alice.asksFor(Presence.of("#modal"));

// Check if absent
boolean isAbsent = alice.asksFor(Absence.of("#error-message"));

Attributes​

Get attribute values:

String href = alice.asksFor(Attribute.of("#link").named("href"));
String placeholder = alice.asksFor(Attribute.of("#input").named("placeholder"));

Current Page Information​

Query page-level information:

String url = alice.asksFor(CurrentUrl.ofThePage());
String title = alice.asksFor(PageTitle.ofThePage());

Network Interception and Mocking​

Playwright provides powerful network interception capabilities for testing.

Intercepting Requests​

import net.serenitybdd.screenplay.playwright.network.InterceptNetwork;

// Intercept and fulfill with mock response
alice.attemptsTo(
InterceptNetwork.requestsTo("**/api/users")
.andRespondWith(
new Route.FulfillOptions()
.setStatus(200)
.setBody("{\"users\": []}")
.setContentType("application/json")
)
);

// Intercept and respond with JSON
alice.attemptsTo(
InterceptNetwork.requestsTo("**/api/user/123")
.andRespondWithJson(200, Map.of(
"id", 123,
"name", "John Doe",
"email", "john@example.com"
))
);

Custom Request Handlers​

For more control, use custom handlers:

alice.attemptsTo(
InterceptNetwork.requestsTo("**/api/**")
.andHandle(route -> {
if (route.request().method().equals("DELETE")) {
route.fulfill(new Route.FulfillOptions()
.setStatus(403)
.setBody("{\"error\": \"Forbidden\"}"));
} else {
route.resume();
}
})
);

Aborting Requests​

Block specific requests (useful for testing error handling):

alice.attemptsTo(
InterceptNetwork.requestsTo("**/analytics/**").andAbort()
);

Removing Routes​

Remove previously registered route handlers:

import net.serenitybdd.screenplay.playwright.network.RemoveRoutes;

// Remove all route handlers
alice.attemptsTo(RemoveRoutes.all());

// Remove routes for specific pattern
alice.attemptsTo(RemoveRoutes.forUrl("**/api/**"));

API Testing Integration​

New in 5.2.2

API testing integration was added in Serenity BDD 5.2.2.

Make API requests that share the browser's session context (cookies, authentication). This enables hybrid UI + API testing scenarios where you can set up data via API, perform UI actions, and verify state through API calls.

Basic API Requests​

import net.serenitybdd.screenplay.playwright.interactions.api.APIRequest;
import net.serenitybdd.screenplay.playwright.questions.api.LastAPIResponse;

// Initialize browser context first (required for API requests)
alice.attemptsTo(Open.url("about:blank"));

// GET request
alice.attemptsTo(
APIRequest.get("https://api.example.com/users/1")
);

// POST request with JSON body
alice.attemptsTo(
APIRequest.post("https://api.example.com/users")
.withJsonBody(Map.of(
"name", "John Doe",
"email", "john@example.com"
))
);

// PUT request
alice.attemptsTo(
APIRequest.put("https://api.example.com/users/1")
.withJsonBody(Map.of("name", "Jane Doe"))
);

// PATCH request
alice.attemptsTo(
APIRequest.patch("https://api.example.com/users/1")
.withJsonBody(Map.of("status", "active"))
);

// DELETE request
alice.attemptsTo(
APIRequest.delete("https://api.example.com/users/1")
);

// HEAD request
alice.attemptsTo(
APIRequest.head("https://api.example.com/users/1")
);

Request Configuration​

// Add custom headers
alice.attemptsTo(
APIRequest.get("https://api.example.com/data")
.withHeader("Authorization", "Bearer token123")
.withHeader("X-Custom-Header", "value")
);

// Add query parameters
alice.attemptsTo(
APIRequest.get("https://api.example.com/search")
.withQueryParam("q", "test")
.withQueryParam("limit", "10")
);

// Set content type
alice.attemptsTo(
APIRequest.post("https://api.example.com/data")
.withBody("<xml>data</xml>")
.withContentType("application/xml")
);

// Set timeout
alice.attemptsTo(
APIRequest.get("https://api.example.com/slow")
.withTimeout(30000) // 30 seconds
);

// Fail on non-2xx status codes
alice.attemptsTo(
APIRequest.get("https://api.example.com/data")
.failOnStatusCode(true)
);

Querying Responses​

// Get status code
int status = alice.asksFor(LastAPIResponse.statusCode());

// Check if response is OK (2xx)
boolean isOk = alice.asksFor(LastAPIResponse.ok());

// Get response body as string
String body = alice.asksFor(LastAPIResponse.body());

// Parse JSON response as Map
Map<String, Object> json = alice.asksFor(LastAPIResponse.jsonBody());

// Parse JSON array response as List
List<Map<String, Object>> items = alice.asksFor(LastAPIResponse.jsonBodyAsList());

// Get response headers
Map<String, String> headers = alice.asksFor(LastAPIResponse.headers());
String contentType = alice.asksFor(LastAPIResponse.header("Content-Type"));

// Get final URL (after redirects)
String url = alice.asksFor(LastAPIResponse.url());

Hybrid UI + API Testing​

API requests automatically share cookies with the browser context:

@Test
void shouldUseAuthenticatedSession() {
// Login via UI
alice.attemptsTo(
Navigate.to("https://myapp.com/login"),
Enter.theValue("user@example.com").into("#email"),
Enter.theValue("password").into("#password"),
Click.on(Button.withText("Login"))
);

// API calls now include authentication cookies
alice.attemptsTo(
APIRequest.get("https://myapp.com/api/profile")
);

// Verify authenticated API response
Map<String, Object> profile = alice.asksFor(LastAPIResponse.jsonBody());
assertThat(profile.get("email")).isEqualTo("user@example.com");
}

API Calls in Serenity Reports​

API requests made via APIRequest are automatically recorded in Serenity reports, similar to RestAssured. The reports show:

  • HTTP method and URL
  • Request headers and body
  • Response status code
  • Response headers and body

This provides full visibility into API interactions during test execution.

Complete Example​

@Test
void shouldCreateAndVerifyUser() {
alice.attemptsTo(Open.url("about:blank"));

// Create user via API
alice.attemptsTo(
APIRequest.post("https://jsonplaceholder.typicode.com/users")
.withJsonBody(Map.of(
"name", "Test User",
"email", "test@example.com",
"username", "testuser"
))
);

// Verify creation
assertThat(alice.asksFor(LastAPIResponse.statusCode())).isEqualTo(201);

Map<String, Object> createdUser = alice.asksFor(LastAPIResponse.jsonBody());
assertThat(createdUser.get("name")).isEqualTo("Test User");
assertThat(createdUser.get("id")).isNotNull();

// Fetch the created user
String userId = String.valueOf(((Double) createdUser.get("id")).intValue());
alice.attemptsTo(
APIRequest.get("https://jsonplaceholder.typicode.com/users/" + userId)
);

assertThat(alice.asksFor(LastAPIResponse.statusCode())).isEqualTo(200);
}

Handling Downloads​

Wait for and handle file downloads:

import net.serenitybdd.screenplay.playwright.interactions.WaitForDownload;
import net.serenitybdd.screenplay.playwright.questions.DownloadedFile;

// Wait for download triggered by clicking
alice.attemptsTo(
WaitForDownload.whilePerforming(Click.on("#download-btn"))
);

// Query download information
String filename = alice.asksFor(DownloadedFile.suggestedFilename());
String url = alice.asksFor(DownloadedFile.url());
Path path = alice.asksFor(DownloadedFile.path());

// Check for failures
String error = alice.asksFor(DownloadedFile.failure());
if (error == null) {
// Download succeeded
}

// Save to specific location
alice.attemptsTo(
WaitForDownload.whilePerforming(Click.on("#export-btn"))
.andSaveTo(Paths.get("/downloads/report.pdf"))
);

Console Message Capture​

Capture and query browser console messages for debugging:

import net.serenitybdd.screenplay.playwright.interactions.CaptureConsoleMessages;
import net.serenitybdd.screenplay.playwright.questions.ConsoleMessages;

// Start capturing console messages
alice.attemptsTo(CaptureConsoleMessages.duringTest());

// ... perform actions that may log to console ...

// Query captured messages
List<String> allMessages = alice.asksFor(ConsoleMessages.all());
List<String> errors = alice.asksFor(ConsoleMessages.errors());
List<String> warnings = alice.asksFor(ConsoleMessages.warnings());
List<String> logs = alice.asksFor(ConsoleMessages.logs());

// Filter by content
List<String> apiErrors = alice.asksFor(ConsoleMessages.containing("API error"));

// Get message counts
int totalCount = alice.asksFor(ConsoleMessages.count());
int errorCount = alice.asksFor(ConsoleMessages.errorCount());

// Clear captured messages between test phases
alice.attemptsTo(CaptureConsoleMessages.clear());

Checking for Console Errors​

New in 5.2.2

CheckConsole was added in Serenity BDD 5.2.2.

Use CheckConsole to automatically fail tests when JavaScript errors or warnings occur. This is the recommended way to ensure your application doesn't have console errors during user flows:

import net.serenitybdd.screenplay.playwright.interactions.CheckConsole;

// Start capturing, then check for errors at the end of the flow
alice.attemptsTo(
CaptureConsoleMessages.duringTest(),

// Perform user actions
Navigate.to("https://myapp.com/checkout"),
Enter.theValue("4111111111111111").into("#card-number"),
Click.on(Button.withText("Pay")),

// Fail the test if any JavaScript errors occurred
CheckConsole.forErrors()
);

CheckConsole Options​

MethodDescription
CheckConsole.forErrors()Fail if any console errors are found
CheckConsole.forWarnings()Fail if any console warnings are found
CheckConsole.forErrorsAndWarnings()Fail if any errors OR warnings are found

Report-Only Mode​

Sometimes you want to document console issues without failing the test (e.g., for known issues or when monitoring error trends):

// Report errors to Serenity but don't fail the test
alice.attemptsTo(
CheckConsole.forErrors().andReportOnly()
);

When errors are found, they are automatically attached to the Serenity report as evidence, whether the test fails or not.

Reporting Console Messages​

New in 5.2.2

ReportConsoleMessages was added in Serenity BDD 5.2.2.

Use ReportConsoleMessages to explicitly add captured console messages to the Serenity report:

import net.serenitybdd.screenplay.playwright.interactions.ReportConsoleMessages;

alice.attemptsTo(
CaptureConsoleMessages.duringTest(),

// ... perform actions ...

// Report errors and warnings to Serenity
ReportConsoleMessages.errorsAndWarnings()
);

ReportConsoleMessages Options​

MethodDescription
ReportConsoleMessages.all()Report all console messages
ReportConsoleMessages.errors()Report only errors
ReportConsoleMessages.warnings()Report only warnings
ReportConsoleMessages.errorsAndWarnings()Report errors and warnings

Example: Complete Console Error Checking​

Here's a typical pattern for ensuring no JavaScript errors occur during a critical user flow:

@Test
void checkoutFlowShouldHaveNoJavaScriptErrors() {
alice.attemptsTo(
// Start capturing at the beginning
CaptureConsoleMessages.duringTest(),

// Complete the checkout flow
Navigate.to("https://myapp.com/cart"),
Click.on(Button.withText("Checkout")),
Enter.theValue("john@example.com").into(InputField.withLabel("Email")),
Enter.theValue("4111111111111111").into(InputField.withLabel("Card Number")),
Click.on(Button.withText("Place Order")),

// Verify order confirmation
WaitFor.theElement(".order-confirmation").toBeVisible(),

// Fail the test if any JavaScript errors occurred during the flow
CheckConsole.forErrors()
);
}

Network Request Capture​

New in 5.2.2

Network request capture was added in Serenity BDD 5.2.2.

Capture and analyze all network requests made during tests. This is useful for debugging, verifying API calls made by the frontend, and detecting failed requests.

import net.serenitybdd.screenplay.playwright.interactions.CaptureNetworkRequests;
import net.serenitybdd.screenplay.playwright.interactions.CaptureNetworkRequests.CapturedRequest;
import net.serenitybdd.screenplay.playwright.questions.NetworkRequests;

// Start capturing network requests
alice.attemptsTo(CaptureNetworkRequests.duringTest());

// Perform actions that trigger network requests
alice.attemptsTo(Navigate.to("https://example.com"));

// Query all captured requests
List<CapturedRequest> allRequests = alice.asksFor(NetworkRequests.all());
int requestCount = alice.asksFor(NetworkRequests.count());

// Filter by HTTP method
List<CapturedRequest> getRequests = alice.asksFor(NetworkRequests.withMethod("GET"));
List<CapturedRequest> postRequests = alice.asksFor(NetworkRequests.withMethod("POST"));

// Filter by URL
List<CapturedRequest> apiRequests = alice.asksFor(
NetworkRequests.toUrlContaining("/api/")
);

// Filter by glob pattern
List<CapturedRequest> cssRequests = alice.asksFor(
NetworkRequests.matching("**/*.css")
);

// Find failed requests (4xx, 5xx, or network errors)
List<CapturedRequest> failedRequests = alice.asksFor(NetworkRequests.failed());
int failedCount = alice.asksFor(NetworkRequests.failedCount());

// Find client errors (4xx)
List<CapturedRequest> clientErrors = alice.asksFor(NetworkRequests.clientErrors());

// Find server errors (5xx)
List<CapturedRequest> serverErrors = alice.asksFor(NetworkRequests.serverErrors());

// Clear captured requests between test phases
alice.attemptsTo(CaptureNetworkRequests.clear());

CapturedRequest Properties​

Each CapturedRequest contains:

PropertyDescription
getUrl()The request URL
getMethod()HTTP method (GET, POST, etc.)
getResourceType()Resource type (document, xhr, fetch, stylesheet, etc.)
getRequestHeaders()Map of request headers
getStatus()Response status code (or null if pending/failed)
getStatusText()Response status text
getFailureText()Failure reason for network errors
isFailed()True if request failed (4xx, 5xx, or network error)
isClientError()True if 4xx status
isServerError()True if 5xx status

Example: Verifying API Calls​

@Test
void shouldMakeCorrectApiCalls() {
alice.attemptsTo(
CaptureNetworkRequests.duringTest(),
Navigate.to("https://myapp.com"),
Click.on(Button.withText("Load Data"))
);

// Verify the expected API call was made
List<CapturedRequest> apiCalls = alice.asksFor(
NetworkRequests.toUrlContaining("/api/data")
);

assertThat(apiCalls).hasSize(1);
assertThat(apiCalls.get(0).getMethod()).isEqualTo("GET");
assertThat(apiCalls.get(0).getStatus()).isEqualTo(200);
}

Failure Evidence Capture​

New in 5.2.2

Automatic failure evidence capture was added in Serenity BDD 5.2.2.

When a test fails, Serenity Playwright automatically captures diagnostic information and attaches it to the report. This makes debugging test failures much easier.

Automatic Evidence Collection​

When console message or network request capture is enabled, the following evidence is automatically collected on test failure:

  • Page Information: Current URL and page title
  • Console Errors: All console.error() and console.warn() messages
  • Failed Network Requests: Requests that returned 4xx/5xx status or network errors

This evidence is attached to the Serenity report as "Playwright Failure Evidence".

Enabling Evidence Capture​

Simply enable console and/or network capture at the start of your tests:

@BeforeEach
void setup(Page page) {
alice = Actor.named("Alice");
alice.can(BrowseTheWebWithPlaywright.withPage(page));

// Enable capture for failure evidence
alice.attemptsTo(
CaptureConsoleMessages.duringTest(),
CaptureNetworkRequests.duringTest()
);
}

Programmatic Access to Evidence​

You can also query evidence programmatically for assertions or custom reporting:

import net.serenitybdd.screenplay.playwright.evidence.PlaywrightFailureEvidence;

// Get console errors
List<String> consoleErrors = PlaywrightFailureEvidence.getConsoleErrors(alice);

// Get failed network requests
List<String> failedRequests = PlaywrightFailureEvidence.getFailedRequests(alice);

// Use in assertions
assertThat(consoleErrors).isEmpty();
assertThat(failedRequests).isEmpty();

Example: Detecting JavaScript Errors​

@Test
void pageShouldNotHaveJavaScriptErrors() {
alice.attemptsTo(
CaptureConsoleMessages.duringTest(),
Navigate.to("https://myapp.com"),
Click.on(Button.withText("Submit"))
);

// Verify no JavaScript errors occurred
List<String> errors = alice.asksFor(ConsoleMessages.errors());
assertThat(errors)
.describedAs("JavaScript errors on page")
.isEmpty();
}

Example: Detecting Failed API Calls​

@Test
void allApiCallsShouldSucceed() {
alice.attemptsTo(
CaptureNetworkRequests.duringTest(),
Navigate.to("https://myapp.com/dashboard")
);

// Verify no API calls failed
List<CapturedRequest> failedRequests = alice.asksFor(NetworkRequests.failed());
assertThat(failedRequests)
.describedAs("Failed network requests")
.isEmpty();
}

Accessibility Testing​

Test accessibility compliance using ARIA snapshots:

import net.serenitybdd.screenplay.playwright.questions.AccessibilitySnapshot;
import com.microsoft.playwright.options.AriaRole;

// Get accessibility snapshot of the entire page
String pageSnapshot = alice.asksFor(AccessibilitySnapshot.ofThePage());

// Get accessibility snapshot of a specific element
String navSnapshot = alice.asksFor(AccessibilitySnapshot.of("#main-nav"));
String formSnapshot = alice.asksFor(AccessibilitySnapshot.of(LOGIN_FORM));

// Get all elements with a specific ARIA role
List<String> buttons = alice.asksFor(AccessibilitySnapshot.allWithRole(AriaRole.BUTTON));
List<String> links = alice.asksFor(AccessibilitySnapshot.allWithRole(AriaRole.LINK));
List<String> headings = alice.asksFor(AccessibilitySnapshot.allWithRole(AriaRole.HEADING));

Visual Regression Testing​

Compare screenshots against baseline images:

import net.serenitybdd.screenplay.playwright.assertions.visual.CompareScreenshot;

// Full page comparison
alice.attemptsTo(
CompareScreenshot.ofPage()
.againstBaseline("homepage-baseline.png")
.withThreshold(0.01) // 1% difference allowed
);

// Element comparison
alice.attemptsTo(
CompareScreenshot.of("#product-card")
.againstBaseline("product-card-baseline.png")
);

// With mask for dynamic content
alice.attemptsTo(
CompareScreenshot.ofPage()
.againstBaseline("dashboard.png")
.withMask("#timestamp", "#user-avatar")
);

Device Emulation​

Test responsive designs with device emulation:

import net.serenitybdd.screenplay.playwright.interactions.EmulateDevice;

// Emulate specific device
alice.attemptsTo(EmulateDevice.device("iPhone 14"));
alice.attemptsTo(EmulateDevice.device("Pixel 7"));
alice.attemptsTo(EmulateDevice.device("iPad Pro 11"));

// Custom viewport
alice.attemptsTo(EmulateDevice.withViewport(375, 812));

// With device scale factor
alice.attemptsTo(
EmulateDevice.withViewport(375, 812).andDeviceScaleFactor(2)
);

// With mobile user agent
alice.attemptsTo(
EmulateDevice.withViewport(375, 812).asMobile()
);

Geolocation​

Test location-based features:

import net.serenitybdd.screenplay.playwright.interactions.SetGeolocation;
import net.serenitybdd.screenplay.playwright.interactions.GrantPermissions;

// First grant geolocation permission
alice.attemptsTo(GrantPermissions.for_("geolocation"));

// Set specific coordinates
alice.attemptsTo(SetGeolocation.to(51.5074, -0.1278)); // London

// Use predefined locations
alice.attemptsTo(SetGeolocation.toNewYork());
alice.attemptsTo(SetGeolocation.toLondon());
alice.attemptsTo(SetGeolocation.toTokyo());
alice.attemptsTo(SetGeolocation.toSanFrancisco());
alice.attemptsTo(SetGeolocation.toSydney());
alice.attemptsTo(SetGeolocation.toParis());

// With accuracy
alice.attemptsTo(
SetGeolocation.to(40.7128, -74.0060).withAccuracy(100)
);

// Clear geolocation
alice.attemptsTo(SetGeolocation.clear());

Permission Management​

Grant or clear browser permissions:

import net.serenitybdd.screenplay.playwright.interactions.GrantPermissions;
import net.serenitybdd.screenplay.playwright.interactions.ClearPermissions;

// Grant specific permissions
alice.attemptsTo(GrantPermissions.for_("geolocation"));
alice.attemptsTo(GrantPermissions.for_("notifications", "camera", "microphone"));

// Clear all permissions
alice.attemptsTo(ClearPermissions.all());

Clock Control​

Test time-dependent functionality:

import net.serenitybdd.screenplay.playwright.interactions.ControlClock;
import java.time.Instant;

// Install fake clock
alice.attemptsTo(ControlClock.install());

// Set to specific time
alice.attemptsTo(
ControlClock.setTo(Instant.parse("2024-01-15T10:30:00Z"))
);

// Advance time
alice.attemptsTo(ControlClock.advanceBy(Duration.ofHours(2)));
alice.attemptsTo(ControlClock.advanceBy(Duration.ofMinutes(30)));

// Resume normal time flow
alice.attemptsTo(ControlClock.resume());

Tracing​

Record detailed traces for debugging:

import net.serenitybdd.screenplay.playwright.interactions.tracing.StartTracing;
import net.serenitybdd.screenplay.playwright.interactions.tracing.StopTracing;

// Start tracing with options
alice.attemptsTo(
StartTracing.withScreenshots()
.andSnapshots()
.named("login-flow")
);

// ... perform test actions ...

// Stop and save trace
alice.attemptsTo(
StopTracing.andSaveTo(Paths.get("traces/login-flow.zip"))
);

// View trace: npx playwright show-trace traces/login-flow.zip

Trace options:

  • withScreenshots() - Include screenshots in trace
  • andSnapshots() - Include DOM snapshots
  • andSources() - Include source files
  • named(String) - Set trace name

Session State Persistence​

New in 5.2.2

Session state persistence was added in Serenity BDD 5.2.2.

Save and restore browser session state (cookies, localStorage, sessionStorage) to speed up tests and share authenticated sessions.

Saving Session State​

import net.serenitybdd.screenplay.playwright.interactions.SaveSessionState;

// Save to a specific path
Path sessionPath = Paths.get("target/sessions/authenticated.json");
alice.attemptsTo(
SaveSessionState.toPath(sessionPath)
);

// Save to default location with a name
// Saves to: target/playwright/session-state/{name}.json
alice.attemptsTo(
SaveSessionState.toFile("admin-session")
);

Restoring Session State​

import net.serenitybdd.screenplay.playwright.interactions.RestoreSessionState;

// Restore from a specific path
alice.attemptsTo(
RestoreSessionState.fromPath(Paths.get("target/sessions/authenticated.json"))
);

// Restore from default location
alice.attemptsTo(
RestoreSessionState.fromFile("admin-session")
);

Use Case: Reusing Authenticated Sessions​

A common pattern is to log in once and reuse the session across multiple tests:

public class AuthenticationSetup {

private static final Path SESSION_FILE = Paths.get("target/sessions/logged-in.json");

@BeforeAll
static void setupAuthenticatedSession() {
Actor setup = Actor.named("Setup")
.whoCan(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration());

setup.attemptsTo(
Navigate.to("https://myapp.com/login"),
Enter.theValue("admin@example.com").into("#email"),
Enter.theValue("password123").into("#password"),
Click.on(Button.withText("Login")),

// Save the authenticated session
SaveSessionState.toPath(SESSION_FILE)
);

setup.wrapUp();
}
}

// In your tests
@Test
void shouldAccessDashboard() {
alice.attemptsTo(
// Restore the pre-authenticated session
RestoreSessionState.fromPath(SESSION_FILE),

// Navigate directly to protected page
Navigate.to("https://myapp.com/dashboard")
);

// User is already logged in!
assertThat(alice.asksFor(Text.of("h1"))).isEqualTo("Dashboard");
}

Session State Contents​

The saved session state is a JSON file containing:

  • Cookies: All cookies for the browser context
  • Origins: localStorage and sessionStorage data for each origin

This allows you to:

  • Skip login steps in tests (faster execution)
  • Share authentication across test classes
  • Create session fixtures for different user roles
  • Test session expiration and refresh scenarios

Example: Multiple User Roles​

public class SessionFixtures {

public static void createAdminSession() {
// Login as admin and save session
Actor admin = createActor();
admin.attemptsTo(
Navigate.to("https://myapp.com/login"),
Enter.theValue("admin@example.com").into("#email"),
Enter.theValue("adminpass").into("#password"),
Click.on(Button.withText("Login")),
SaveSessionState.toFile("admin-session")
);
admin.wrapUp();
}

public static void createUserSession() {
// Login as regular user and save session
Actor user = createActor();
user.attemptsTo(
Navigate.to("https://myapp.com/login"),
Enter.theValue("user@example.com").into("#email"),
Enter.theValue("userpass").into("#password"),
Click.on(Button.withText("Login")),
SaveSessionState.toFile("user-session")
);
user.wrapUp();
}
}

// In admin tests
@Test
void adminShouldSeeAdminPanel() {
alice.attemptsTo(
RestoreSessionState.fromFile("admin-session"),
Navigate.to("https://myapp.com/admin")
);
assertThat(alice.asksFor(Presence.of("#admin-panel"))).isTrue();
}

// In user tests
@Test
void userShouldNotSeeAdminPanel() {
alice.attemptsTo(
RestoreSessionState.fromFile("user-session"),
Navigate.to("https://myapp.com/admin")
);
assertThat(alice.asksFor(Text.of("h1"))).isEqualTo("Access Denied");
}

PDF Generation​

Generate PDFs from pages (Chromium only, headless mode):

import net.serenitybdd.screenplay.playwright.interactions.GeneratePDF;

// Basic PDF generation
alice.attemptsTo(
GeneratePDF.ofCurrentPage()
.andSaveTo(Paths.get("output/report.pdf"))
);

// With options
alice.attemptsTo(
GeneratePDF.ofCurrentPage()
.withFormat("A4")
.inLandscape()
.withMargins("1cm", "1cm", "1cm", "1cm")
.withHeaderTemplate("<div>Header</div>")
.withFooterTemplate("<div>Page <span class='pageNumber'></span></div>")
.displayHeaderAndFooter()
.printBackground()
.andSaveTo(Paths.get("output/report.pdf"))
);

Switching Contexts​

Frames​

Work with iframes:

// Switch to frame by name or ID
alice.attemptsTo(Switch.toFrame("payment-iframe"));

// Switch to frame by Target
alice.attemptsTo(Switch.toFrame(Target.the("payment").locatedBy("#payment-frame")));

// Switch back to main frame
alice.attemptsTo(Switch.toMainFrame());

Windows and Tabs​

Handle multiple windows and tabs:

// Switch to new window/tab
alice.attemptsTo(Switch.toNewWindow());

// Close current window
alice.attemptsTo(CloseCurrentWindow.now());

Best Practices​

Use Target Constants​

Define targets as constants for reusability and maintainability:

public class LoginPage {
public static Target EMAIL_FIELD = Target.the("email field")
.locatedBy("#email");
public static Target PASSWORD_FIELD = Target.the("password field")
.locatedBy("#password");
public static Target LOGIN_BUTTON = Target.the("login button")
.locatedBy("role=button[name='Log in']");
}

Prefer Role Selectors​

Use ARIA role selectors for more resilient tests:

// Instead of CSS
Target.the("Submit").locatedBy("button.primary-btn")

// Prefer role selector
Target.the("Submit").locatedBy("role=button[name='Submit']")

Use UI Element Factories​

For common elements, use the UI element factories:

// Instead of
Click.on(Target.the("Add button").locatedBy("role=button[name='Add to Cart']"))

// Use
Click.on(Button.withText("Add to Cart"))

Network Mocking for Isolation​

Mock API responses to isolate UI tests:

@BeforeEach
void setupMocks() {
actor.attemptsTo(
InterceptNetwork.requestsTo("**/api/products")
.andRespondWithJson(200, mockProducts)
);
}

Use Tracing for Debugging​

Enable tracing when debugging test failures:

alice.attemptsTo(
StartTracing.withScreenshots().andSnapshots()
);

// Run your test...

alice.attemptsTo(
StopTracing.andSaveTo(Paths.get("trace.zip"))
);
// View with: npx playwright show-trace trace.zip

Quick Reference Tables​

All Interactions​

The following tables provide a complete reference of all available Playwright Screenplay interactions.

Element Interactions​

InteractionDescriptionExample
Click.on(target)Click on an elementClick.on("#submit")
DoubleClick.on(target)Double-click on an elementDoubleClick.on("#item")
RightClick.on(target)Right-click (context menu) on an elementRightClick.on("#file")
Hover.over(target)Move mouse over an elementHover.over("#menu")
Focus.on(target)Set focus to an elementFocus.on("#search")

Text Input Interactions​

InteractionDescriptionExample
Enter.theValue(text).into(target)Type text into a fieldEnter.theValue("john@test.com").into("#email")
Clear.field(target)Clear an input fieldClear.field("#search")
Press.key(key)Press a keyboard keyPress.key("Enter")
Press.key(combo)Press a key combinationPress.key("Control+a")
Press.keys(keys...)Press multiple keys in sequencePress.keys("Tab", "Tab", "Enter")

Checkbox & Radio Interactions​

InteractionDescriptionExample
Check.checkbox(target)Check a checkboxCheck.checkbox("#agree")
Uncheck.checkbox(target)Uncheck a checkboxUncheck.checkbox("#newsletter")
InteractionDescriptionExample
SelectFromOptions.byVisibleText(text).from(target)Select by visible textSelectFromOptions.byVisibleText("Red").from("#color")
SelectFromOptions.byValue(value).from(target)Select by value attributeSelectFromOptions.byValue("red").from("#color")
SelectFromOptions.byIndex(index).from(target)Select by index (0-based)SelectFromOptions.byIndex(2).from("#color")
DeselectFromOptions.byValue(value).from(target)Deselect by valueDeselectFromOptions.byValue("red").from("#colors")
DeselectFromOptions.byVisibleText(text).from(target)Deselect by visible textDeselectFromOptions.byVisibleText("Red").from("#colors")
DeselectFromOptions.byIndex(index).from(target)Deselect by indexDeselectFromOptions.byIndex(0).from("#colors")
DeselectFromOptions.all().from(target)Deselect all optionsDeselectFromOptions.all().from("#colors")

Scrolling Interactions​

InteractionDescriptionExample
Scroll.to(target)Scroll element into viewScroll.to("#footer")
Scroll.to(target).andAlignToTop()Scroll with top alignmentScroll.to("#section").andAlignToTop()
Scroll.to(target).andAlignToCenter()Scroll with center alignmentScroll.to("#section").andAlignToCenter()
Scroll.to(target).andAlignToBottom()Scroll with bottom alignmentScroll.to("#section").andAlignToBottom()
Scroll.toTop()Scroll to page topScroll.toTop()
Scroll.toBottom()Scroll to page bottomScroll.toBottom()
Scroll.by(deltaX, deltaY)Scroll by pixel amountScroll.by(0, 500)
Scroll.toPosition(x, y)Scroll to absolute positionScroll.toPosition(0, 1000)

Drag and Drop Interactions​

InteractionDescriptionExample
Drag.from(source).to(target)Drag from source to targetDrag.from("#item").to("#dropzone")
Drag.the(source).onto(target)Alternative fluent syntaxDrag.the("#card").onto("#column")
InteractionDescriptionExample
Navigate.to(url)Navigate to a URLNavigate.to("https://example.com")
Navigate.toTheBaseUrl()Navigate to configured base URLNavigate.toTheBaseUrl()

Frame & Window Interactions​

InteractionDescriptionExample
Switch.toFrame(nameOrId)Switch to iframe by name/IDSwitch.toFrame("payment-iframe")
Switch.toFrame(target)Switch to iframe by TargetSwitch.toFrame(PAYMENT_FRAME)
Switch.toMainFrame()Switch back to main frameSwitch.toMainFrame()
Switch.toNewWindow()Switch to new window/tabSwitch.toNewWindow()
CloseCurrentWindow.now()Close current windowCloseCurrentWindow.now()

File Interactions​

InteractionDescriptionExample
Upload.file(path).to(target)Upload a fileUpload.file(Paths.get("doc.pdf")).to("#upload")
WaitForDownload.whilePerforming(action)Wait for download during actionWaitForDownload.whilePerforming(Click.on("#download"))
WaitForDownload...andSaveTo(path)Save download to pathWaitForDownload.whilePerforming(...).andSaveTo(path)

JavaScript Interactions​

InteractionDescriptionExample
Evaluate.javascript(script)Execute JavaScriptEvaluate.javascript("window.scrollTo(0,0)")

Wait Interactions​

InteractionDescriptionExample
WaitUntil.the(target).isVisible()Wait for element visibilityWaitUntil.the("#modal").isVisible()
WaitUntil.the(target).isNotVisible()Wait for element to hideWaitUntil.the("#spinner").isNotVisible()
WaitUntil.the(target).isHidden()Wait for element to be hiddenWaitUntil.the("#loading").isHidden()
WaitUntil...forNoMoreThan(duration)Set custom timeoutWaitUntil.the("#data").isVisible().forNoMoreThan(Duration.ofSeconds(10))

Network Interactions​

InteractionDescriptionExample
InterceptNetwork.requestsTo(pattern).andRespondWith(options)Mock response with optionsInterceptNetwork.requestsTo("**/api/**").andRespondWith(...)
InterceptNetwork.requestsTo(pattern).andRespondWithJson(status, data)Mock JSON responseInterceptNetwork.requestsTo("**/users").andRespondWithJson(200, users)
InterceptNetwork.requestsTo(pattern).andHandle(handler)Custom request handlerInterceptNetwork.requestsTo("**/api/**").andHandle(route -> ...)
InterceptNetwork.requestsTo(pattern).andAbort()Block requestsInterceptNetwork.requestsTo("**/analytics/**").andAbort()
RemoveRoutes.all()Remove all route handlersRemoveRoutes.all()
RemoveRoutes.forUrl(pattern)Remove routes for patternRemoveRoutes.forUrl("**/api/**")

Device & Environment Interactions​

InteractionDescriptionExample
EmulateDevice.device(name)Emulate a deviceEmulateDevice.device("iPhone 14")
EmulateDevice.withViewport(width, height)Set custom viewportEmulateDevice.withViewport(375, 812)
EmulateDevice...andDeviceScaleFactor(factor)Set device scaleEmulateDevice.withViewport(375, 812).andDeviceScaleFactor(2)
EmulateDevice...asMobile()Add mobile user agentEmulateDevice.withViewport(375, 812).asMobile()
SetGeolocation.to(lat, lng)Set geolocationSetGeolocation.to(51.5074, -0.1278)
SetGeolocation.to(lat, lng).withAccuracy(m)Set geolocation with accuracySetGeolocation.to(40.7128, -74.0060).withAccuracy(100)
SetGeolocation.toNewYork()Set to New YorkSetGeolocation.toNewYork()
SetGeolocation.toLondon()Set to LondonSetGeolocation.toLondon()
SetGeolocation.toTokyo()Set to TokyoSetGeolocation.toTokyo()
SetGeolocation.toSanFrancisco()Set to San FranciscoSetGeolocation.toSanFrancisco()
SetGeolocation.toSydney()Set to SydneySetGeolocation.toSydney()
SetGeolocation.toParis()Set to ParisSetGeolocation.toParis()
SetGeolocation.clear()Clear geolocationSetGeolocation.clear()
GrantPermissions.for_(permissions...)Grant browser permissionsGrantPermissions.for_("geolocation", "camera")
ClearPermissions.all()Clear all permissionsClearPermissions.all()

Clock Control Interactions​

InteractionDescriptionExample
ControlClock.install()Install fake clockControlClock.install()
ControlClock.setTo(instant)Set clock to specific timeControlClock.setTo(Instant.parse("2024-01-15T10:30:00Z"))
ControlClock.advanceBy(duration)Advance clockControlClock.advanceBy(Duration.ofHours(2))
ControlClock.resume()Resume normal time flowControlClock.resume()

Debugging & Tracing Interactions​

InteractionDescriptionExample
StartTracing.withScreenshots()Start trace with screenshotsStartTracing.withScreenshots()
StartTracing...andSnapshots()Include DOM snapshotsStartTracing.withScreenshots().andSnapshots()
StartTracing...andSources()Include source filesStartTracing.withScreenshots().andSources()
StartTracing...named(name)Set trace nameStartTracing.withScreenshots().named("login-test")
StopTracing.andSaveTo(path)Stop and save traceStopTracing.andSaveTo(Paths.get("trace.zip"))
CaptureConsoleMessages.duringTest()Start capturing consoleCaptureConsoleMessages.duringTest()
CaptureConsoleMessages.clear()Clear captured messagesCaptureConsoleMessages.clear()
CheckConsole.forErrors()Fail if console errors foundCheckConsole.forErrors()
CheckConsole.forWarnings()Fail if console warnings foundCheckConsole.forWarnings()
CheckConsole.forErrorsAndWarnings()Fail if errors or warnings foundCheckConsole.forErrorsAndWarnings()
CheckConsole...andReportOnly()Report without failingCheckConsole.forErrors().andReportOnly()
ReportConsoleMessages.all()Report all console messagesReportConsoleMessages.all()
ReportConsoleMessages.errors()Report console errorsReportConsoleMessages.errors()
ReportConsoleMessages.errorsAndWarnings()Report errors and warningsReportConsoleMessages.errorsAndWarnings()
CaptureNetworkRequests.duringTest()Start capturing networkCaptureNetworkRequests.duringTest()
CaptureNetworkRequests.clear()Clear captured requestsCaptureNetworkRequests.clear()

Session State Interactions​

InteractionDescriptionExample
SaveSessionState.toPath(path)Save session to specific pathSaveSessionState.toPath(Paths.get("session.json"))
SaveSessionState.toFile(name)Save session to default locationSaveSessionState.toFile("admin-session")
RestoreSessionState.fromPath(path)Restore session from pathRestoreSessionState.fromPath(Paths.get("session.json"))
RestoreSessionState.fromFile(name)Restore session from default locationRestoreSessionState.fromFile("admin-session")

API Request Interactions​

InteractionDescriptionExample
APIRequest.get(url)Make GET requestAPIRequest.get("https://api.example.com/users")
APIRequest.post(url)Make POST requestAPIRequest.post("https://api.example.com/users")
APIRequest.put(url)Make PUT requestAPIRequest.put("https://api.example.com/users/1")
APIRequest.patch(url)Make PATCH requestAPIRequest.patch("https://api.example.com/users/1")
APIRequest.delete(url)Make DELETE requestAPIRequest.delete("https://api.example.com/users/1")
APIRequest.head(url)Make HEAD requestAPIRequest.head("https://api.example.com/users/1")
APIRequest...withJsonBody(object)Set JSON request bodyAPIRequest.post(url).withJsonBody(Map.of("name", "John"))
APIRequest...withBody(string)Set string request bodyAPIRequest.post(url).withBody("<xml>data</xml>")
APIRequest...withHeader(name, value)Add request headerAPIRequest.get(url).withHeader("Authorization", "Bearer token")
APIRequest...withQueryParam(name, value)Add query parameterAPIRequest.get(url).withQueryParam("page", "1")
APIRequest...withContentType(type)Set Content-TypeAPIRequest.post(url).withContentType("application/xml")
APIRequest...withTimeout(ms)Set request timeoutAPIRequest.get(url).withTimeout(30000)
APIRequest...failOnStatusCode(boolean)Fail on non-2xx statusAPIRequest.get(url).failOnStatusCode(true)

PDF Generation Interactions​

InteractionDescriptionExample
GeneratePDF.ofCurrentPage().andSaveTo(path)Generate PDFGeneratePDF.ofCurrentPage().andSaveTo(Paths.get("page.pdf"))
GeneratePDF...withFormat(format)Set paper formatGeneratePDF.ofCurrentPage().withFormat("A4")
GeneratePDF...inLandscape()Use landscape orientationGeneratePDF.ofCurrentPage().inLandscape()
GeneratePDF...withMargins(t, r, b, l)Set marginsGeneratePDF.ofCurrentPage().withMargins("1cm", "1cm", "1cm", "1cm")
GeneratePDF...printBackground()Include background graphicsGeneratePDF.ofCurrentPage().printBackground()
GeneratePDF...displayHeaderAndFooter()Show header/footerGeneratePDF.ofCurrentPage().displayHeaderAndFooter()

Visual Testing Interactions​

InteractionDescriptionExample
CompareScreenshot.ofPage().againstBaseline(name)Compare full pageCompareScreenshot.ofPage().againstBaseline("home.png")
CompareScreenshot.of(target).againstBaseline(name)Compare elementCompareScreenshot.of("#card").againstBaseline("card.png")
CompareScreenshot...withThreshold(threshold)Set diff thresholdCompareScreenshot.ofPage().againstBaseline("x.png").withThreshold(0.01)
CompareScreenshot...withMask(targets...)Mask dynamic elementsCompareScreenshot.ofPage().againstBaseline("x.png").withMask("#time")

All Questions​

The following tables provide a complete reference of all available Playwright Screenplay questions.

Element State Questions​

QuestionReturn TypeDescriptionExample
Presence.of(target)BooleanElement exists in DOMactor.asksFor(Presence.of("#modal"))
Absence.of(target)BooleanElement not presentactor.asksFor(Absence.of("#error"))
Visibility.of(target)BooleanElement is visibleactor.asksFor(Visibility.of("#popup"))
Enabled.of(target)BooleanElement is enabledactor.asksFor(Enabled.of("#submit"))
SelectedStatus.of(target)BooleanCheckbox/radio is selectedactor.asksFor(SelectedStatus.of("#agree"))

Element Content Questions​

QuestionReturn TypeDescriptionExample
Text.of(target)StringGet element text contentactor.asksFor(Text.of("#title"))
Text.ofEach(target)List<String>Get text of all matching elementsactor.asksFor(Text.ofEach(".item"))
Value.of(target)StringGet input field valueactor.asksFor(Value.of("#email"))
Attribute.of(target).named(attr)StringGet attribute valueactor.asksFor(Attribute.of("#link").named("href"))
CSSValue.of(target).named(prop)StringGet CSS property valueactor.asksFor(CSSValue.of("#box").named("color"))

Page Information Questions​

QuestionReturn TypeDescriptionExample
CurrentUrl.ofThePage()StringGet current page URLactor.asksFor(CurrentUrl.ofThePage())
PageTitle.ofThePage()StringGet page titleactor.asksFor(PageTitle.ofThePage())

Download Questions​

QuestionReturn TypeDescriptionExample
DownloadedFile.suggestedFilename()StringGet suggested filenameactor.asksFor(DownloadedFile.suggestedFilename())
DownloadedFile.url()StringGet download URLactor.asksFor(DownloadedFile.url())
DownloadedFile.path()PathGet download file pathactor.asksFor(DownloadedFile.path())
DownloadedFile.failure()StringGet failure reason (null if success)actor.asksFor(DownloadedFile.failure())
DownloadedFile.download()DownloadGet Playwright Download objectactor.asksFor(DownloadedFile.download())

Console Message Questions​

QuestionReturn TypeDescriptionExample
ConsoleMessages.all()List<String>Get all console messagesactor.asksFor(ConsoleMessages.all())
ConsoleMessages.errors()List<String>Get console errorsactor.asksFor(ConsoleMessages.errors())
ConsoleMessages.warnings()List<String>Get console warningsactor.asksFor(ConsoleMessages.warnings())
ConsoleMessages.logs()List<String>Get console logsactor.asksFor(ConsoleMessages.logs())
ConsoleMessages.info()List<String>Get console info messagesactor.asksFor(ConsoleMessages.info())
ConsoleMessages.containing(text)List<String>Get messages containing textactor.asksFor(ConsoleMessages.containing("error"))
ConsoleMessages.count()IntegerGet total message countactor.asksFor(ConsoleMessages.count())
ConsoleMessages.errorCount()IntegerGet error countactor.asksFor(ConsoleMessages.errorCount())
ConsoleMessages.allCaptured()List<CapturedConsoleMessage>Get full message objectsactor.asksFor(ConsoleMessages.allCaptured())

Accessibility Questions​

QuestionReturn TypeDescriptionExample
AccessibilitySnapshot.ofThePage()StringGet page accessibility treeactor.asksFor(AccessibilitySnapshot.ofThePage())
AccessibilitySnapshot.of(target)StringGet element accessibility treeactor.asksFor(AccessibilitySnapshot.of("#nav"))
AccessibilitySnapshot.allWithRole(role)List<String>Get elements by ARIA roleactor.asksFor(AccessibilitySnapshot.allWithRole(AriaRole.BUTTON))

Network Request Questions​

QuestionReturn TypeDescriptionExample
NetworkRequests.all()List<CapturedRequest>Get all captured requestsactor.asksFor(NetworkRequests.all())
NetworkRequests.count()IntegerGet total request countactor.asksFor(NetworkRequests.count())
NetworkRequests.withMethod(method)List<CapturedRequest>Filter by HTTP methodactor.asksFor(NetworkRequests.withMethod("POST"))
NetworkRequests.toUrlContaining(text)List<CapturedRequest>Filter by URL substringactor.asksFor(NetworkRequests.toUrlContaining("/api/"))
NetworkRequests.matching(pattern)List<CapturedRequest>Filter by glob patternactor.asksFor(NetworkRequests.matching("**/*.js"))
NetworkRequests.failed()List<CapturedRequest>Get failed requestsactor.asksFor(NetworkRequests.failed())
NetworkRequests.failedCount()IntegerGet failed request countactor.asksFor(NetworkRequests.failedCount())
NetworkRequests.clientErrors()List<CapturedRequest>Get 4xx errorsactor.asksFor(NetworkRequests.clientErrors())
NetworkRequests.serverErrors()List<CapturedRequest>Get 5xx errorsactor.asksFor(NetworkRequests.serverErrors())

API Response Questions​

QuestionReturn TypeDescriptionExample
LastAPIResponse.statusCode()IntegerGet response status codeactor.asksFor(LastAPIResponse.statusCode())
LastAPIResponse.ok()BooleanCheck if status is 2xxactor.asksFor(LastAPIResponse.ok())
LastAPIResponse.body()StringGet response body as stringactor.asksFor(LastAPIResponse.body())
LastAPIResponse.jsonBody()Map<String, Object>Parse JSON response as Mapactor.asksFor(LastAPIResponse.jsonBody())
LastAPIResponse.jsonBodyAsList()List<Map<String, Object>>Parse JSON array responseactor.asksFor(LastAPIResponse.jsonBodyAsList())
LastAPIResponse.headers()Map<String, String>Get all response headersactor.asksFor(LastAPIResponse.headers())
LastAPIResponse.header(name)StringGet specific headeractor.asksFor(LastAPIResponse.header("Content-Type"))
LastAPIResponse.url()StringGet final URL (after redirects)actor.asksFor(LastAPIResponse.url())

UI Element Factories​

Factory classes for locating common UI elements.

FactoryMethodsExample
ButtonwithText(text), withNameOrId(id), withAriaLabel(label), containingText(text), locatedBy(selector)Button.withText("Submit")
InputFieldwithNameOrId(id), withPlaceholder(text), withLabel(label), withAriaLabel(label), locatedBy(selector)InputField.withPlaceholder("Email")
LinkwithText(text), containingText(text), withTitle(title), locatedBy(selector)Link.withText("Learn more")
CheckboxwithLabel(label), withNameOrId(id), withValue(value), locatedBy(selector)Checkbox.withLabel("I agree")
RadioButtonwithLabel(label), withNameOrId(id), withValue(value), locatedBy(selector)RadioButton.withValue("express")
DropdownwithLabel(label), withNameOrId(id), locatedBy(selector)Dropdown.withLabel("Country")
LabelwithText(text), withExactText(text), forFieldId(id), locatedBy(selector)Label.forFieldId("email")
ImagewithAltText(alt), withSrc(src), withSrcContaining(text), locatedBy(selector)Image.withAltText("Logo")

Migration from WebDriver​

When migrating from serenity-screenplay-webdriver:

  1. Ability Change:

    • Replace BrowseTheWeb with BrowseTheWebWithPlaywright
  2. Selector Syntax:

    • CSS and XPath work similarly
    • Add role selectors for more robust tests
    • Use >> for chaining selectors instead of nesting
  3. Waiting:

    • Playwright auto-waits; explicit waits are less necessary
    • Remove most WaitUntil calls
  4. Locator Methods:

    • By.id("x") becomes "#x"
    • By.cssSelector("x") becomes "x"
    • By.xpath("x") becomes "xpath=x"
  5. New Capabilities:

    • Use network interception for mocking APIs
    • Use tracing for debugging
    • Use device emulation for responsive testing

Complete Example​

import com.microsoft.playwright.Page;
import net.serenitybdd.annotations.SerenityPlaywright;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import net.serenitybdd.screenplay.playwright.interactions.*;
import net.serenitybdd.screenplay.playwright.questions.*;
import net.serenitybdd.screenplay.playwright.ui.*;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@SerenityPlaywright
@UsePlaywright
class ShoppingCartTest {

Actor alice;

@BeforeEach
void setup(Page page) {
alice = Actor.named("Alice");
alice.can(BrowseTheWebWithPlaywright.withPage(page));
}

@Test
void shouldAddItemToCart() {
alice.attemptsTo(
Navigate.to("https://shop.example.com"),
Click.on(Button.withText("Electronics")),
Click.on(Link.containingText("Laptop")),
SelectFromOptions.byVisibleText("16GB").from(Dropdown.withLabel("Memory")),
Click.on(Button.withText("Add to Cart"))
);

String cartCount = alice.asksFor(Text.of("#cart-count"));
assertThat(cartCount).isEqualTo("1");

List<String> cartItems = alice.asksFor(Text.ofEach(".cart-item-name"));
assertThat(cartItems).contains("Laptop");
}
}

Further Reading​