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 shorthandYou 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")));
Linkβ
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")));
Dropdownβ
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:
| Interaction | Purpose | Example |
|---|---|---|
| Click | Click on an element | actor.attemptsTo(Click.on("#button")) |
| DoubleClick | Double-click on an element | actor.attemptsTo(DoubleClick.on("#item")) |
| RightClick | Right-click (context menu) | actor.attemptsTo(RightClick.on("#menu")) |
| Enter | Type into an input field | actor.attemptsTo(Enter.theValue("text").into("#field")) |
| Clear | Clear an input field | actor.attemptsTo(Clear.field("#field")) |
| Hover | Hover over an element | actor.attemptsTo(Hover.over("#menu")) |
| Press | Press keyboard keys | actor.attemptsTo(Press.key("Enter")) |
| Check | Check a checkbox | actor.attemptsTo(Check.checkbox("#agree")) |
| Uncheck | Uncheck a checkbox | actor.attemptsTo(Uncheck.checkbox("#agree")) |
| Focus | Focus on an element | actor.attemptsTo(Focus.on("#input")) |
| Navigate | Navigate to a URL | actor.attemptsTo(Navigate.to("https://...")) |
| Upload | Upload a file | actor.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:
| Question | Purpose | Example |
|---|---|---|
| Text | Get element text | actor.asksFor(Text.of("#title")) |
| Value | Get input value | actor.asksFor(Value.of("#email")) |
| Attribute | Get attribute value | actor.asksFor(Attribute.of("#link").named("href")) |
| Presence | Check if element exists | actor.asksFor(Presence.of("#modal")) |
| Absence | Check if element is absent | actor.asksFor(Absence.of("#error")) |
| Visibility | Check if element is visible | actor.asksFor(Visibility.of("#popup")) |
| Enabled | Check if element is enabled | actor.asksFor(Enabled.of("#submit")) |
| SelectedStatus | Check if checkbox is selected | actor.asksFor(SelectedStatus.of("#agree")) |
| CSSValue | Get CSS property | actor.asksFor(CSSValue.of("#box").named("color")) |
| CurrentUrl | Get current page URL | actor.asksFor(CurrentUrl.ofThePage()) |
| PageTitle | Get page title | actor.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β
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β
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β
| Method | Description |
|---|---|
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β
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β
| Method | Description |
|---|---|
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β
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:
| Property | Description |
|---|---|
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β
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()andconsole.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 traceandSnapshots()- Include DOM snapshotsandSources()- Include source filesnamed(String)- Set trace name
Session State Persistenceβ
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β
| Interaction | Description | Example |
|---|---|---|
Click.on(target) | Click on an element | Click.on("#submit") |
DoubleClick.on(target) | Double-click on an element | DoubleClick.on("#item") |
RightClick.on(target) | Right-click (context menu) on an element | RightClick.on("#file") |
Hover.over(target) | Move mouse over an element | Hover.over("#menu") |
Focus.on(target) | Set focus to an element | Focus.on("#search") |
Text Input Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Enter.theValue(text).into(target) | Type text into a field | Enter.theValue("john@test.com").into("#email") |
Clear.field(target) | Clear an input field | Clear.field("#search") |
Press.key(key) | Press a keyboard key | Press.key("Enter") |
Press.key(combo) | Press a key combination | Press.key("Control+a") |
Press.keys(keys...) | Press multiple keys in sequence | Press.keys("Tab", "Tab", "Enter") |
Checkbox & Radio Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Check.checkbox(target) | Check a checkbox | Check.checkbox("#agree") |
Uncheck.checkbox(target) | Uncheck a checkbox | Uncheck.checkbox("#newsletter") |
Dropdown Interactionsβ
| Interaction | Description | Example |
|---|---|---|
SelectFromOptions.byVisibleText(text).from(target) | Select by visible text | SelectFromOptions.byVisibleText("Red").from("#color") |
SelectFromOptions.byValue(value).from(target) | Select by value attribute | SelectFromOptions.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 value | DeselectFromOptions.byValue("red").from("#colors") |
DeselectFromOptions.byVisibleText(text).from(target) | Deselect by visible text | DeselectFromOptions.byVisibleText("Red").from("#colors") |
DeselectFromOptions.byIndex(index).from(target) | Deselect by index | DeselectFromOptions.byIndex(0).from("#colors") |
DeselectFromOptions.all().from(target) | Deselect all options | DeselectFromOptions.all().from("#colors") |
Scrolling Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Scroll.to(target) | Scroll element into view | Scroll.to("#footer") |
Scroll.to(target).andAlignToTop() | Scroll with top alignment | Scroll.to("#section").andAlignToTop() |
Scroll.to(target).andAlignToCenter() | Scroll with center alignment | Scroll.to("#section").andAlignToCenter() |
Scroll.to(target).andAlignToBottom() | Scroll with bottom alignment | Scroll.to("#section").andAlignToBottom() |
Scroll.toTop() | Scroll to page top | Scroll.toTop() |
Scroll.toBottom() | Scroll to page bottom | Scroll.toBottom() |
Scroll.by(deltaX, deltaY) | Scroll by pixel amount | Scroll.by(0, 500) |
Scroll.toPosition(x, y) | Scroll to absolute position | Scroll.toPosition(0, 1000) |
Drag and Drop Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Drag.from(source).to(target) | Drag from source to target | Drag.from("#item").to("#dropzone") |
Drag.the(source).onto(target) | Alternative fluent syntax | Drag.the("#card").onto("#column") |
Navigation Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Navigate.to(url) | Navigate to a URL | Navigate.to("https://example.com") |
Navigate.toTheBaseUrl() | Navigate to configured base URL | Navigate.toTheBaseUrl() |
Frame & Window Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Switch.toFrame(nameOrId) | Switch to iframe by name/ID | Switch.toFrame("payment-iframe") |
Switch.toFrame(target) | Switch to iframe by Target | Switch.toFrame(PAYMENT_FRAME) |
Switch.toMainFrame() | Switch back to main frame | Switch.toMainFrame() |
Switch.toNewWindow() | Switch to new window/tab | Switch.toNewWindow() |
CloseCurrentWindow.now() | Close current window | CloseCurrentWindow.now() |
File Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Upload.file(path).to(target) | Upload a file | Upload.file(Paths.get("doc.pdf")).to("#upload") |
WaitForDownload.whilePerforming(action) | Wait for download during action | WaitForDownload.whilePerforming(Click.on("#download")) |
WaitForDownload...andSaveTo(path) | Save download to path | WaitForDownload.whilePerforming(...).andSaveTo(path) |
JavaScript Interactionsβ
| Interaction | Description | Example |
|---|---|---|
Evaluate.javascript(script) | Execute JavaScript | Evaluate.javascript("window.scrollTo(0,0)") |
Wait Interactionsβ
| Interaction | Description | Example |
|---|---|---|
WaitUntil.the(target).isVisible() | Wait for element visibility | WaitUntil.the("#modal").isVisible() |
WaitUntil.the(target).isNotVisible() | Wait for element to hide | WaitUntil.the("#spinner").isNotVisible() |
WaitUntil.the(target).isHidden() | Wait for element to be hidden | WaitUntil.the("#loading").isHidden() |
WaitUntil...forNoMoreThan(duration) | Set custom timeout | WaitUntil.the("#data").isVisible().forNoMoreThan(Duration.ofSeconds(10)) |
Network Interactionsβ
| Interaction | Description | Example |
|---|---|---|
InterceptNetwork.requestsTo(pattern).andRespondWith(options) | Mock response with options | InterceptNetwork.requestsTo("**/api/**").andRespondWith(...) |
InterceptNetwork.requestsTo(pattern).andRespondWithJson(status, data) | Mock JSON response | InterceptNetwork.requestsTo("**/users").andRespondWithJson(200, users) |
InterceptNetwork.requestsTo(pattern).andHandle(handler) | Custom request handler | InterceptNetwork.requestsTo("**/api/**").andHandle(route -> ...) |
InterceptNetwork.requestsTo(pattern).andAbort() | Block requests | InterceptNetwork.requestsTo("**/analytics/**").andAbort() |
RemoveRoutes.all() | Remove all route handlers | RemoveRoutes.all() |
RemoveRoutes.forUrl(pattern) | Remove routes for pattern | RemoveRoutes.forUrl("**/api/**") |
Device & Environment Interactionsβ
| Interaction | Description | Example |
|---|---|---|
EmulateDevice.device(name) | Emulate a device | EmulateDevice.device("iPhone 14") |
EmulateDevice.withViewport(width, height) | Set custom viewport | EmulateDevice.withViewport(375, 812) |
EmulateDevice...andDeviceScaleFactor(factor) | Set device scale | EmulateDevice.withViewport(375, 812).andDeviceScaleFactor(2) |
EmulateDevice...asMobile() | Add mobile user agent | EmulateDevice.withViewport(375, 812).asMobile() |
SetGeolocation.to(lat, lng) | Set geolocation | SetGeolocation.to(51.5074, -0.1278) |
SetGeolocation.to(lat, lng).withAccuracy(m) | Set geolocation with accuracy | SetGeolocation.to(40.7128, -74.0060).withAccuracy(100) |
SetGeolocation.toNewYork() | Set to New York | SetGeolocation.toNewYork() |
SetGeolocation.toLondon() | Set to London | SetGeolocation.toLondon() |
SetGeolocation.toTokyo() | Set to Tokyo | SetGeolocation.toTokyo() |
SetGeolocation.toSanFrancisco() | Set to San Francisco | SetGeolocation.toSanFrancisco() |
SetGeolocation.toSydney() | Set to Sydney | SetGeolocation.toSydney() |
SetGeolocation.toParis() | Set to Paris | SetGeolocation.toParis() |
SetGeolocation.clear() | Clear geolocation | SetGeolocation.clear() |
GrantPermissions.for_(permissions...) | Grant browser permissions | GrantPermissions.for_("geolocation", "camera") |
ClearPermissions.all() | Clear all permissions | ClearPermissions.all() |
Clock Control Interactionsβ
| Interaction | Description | Example |
|---|---|---|
ControlClock.install() | Install fake clock | ControlClock.install() |
ControlClock.setTo(instant) | Set clock to specific time | ControlClock.setTo(Instant.parse("2024-01-15T10:30:00Z")) |
ControlClock.advanceBy(duration) | Advance clock | ControlClock.advanceBy(Duration.ofHours(2)) |
ControlClock.resume() | Resume normal time flow | ControlClock.resume() |
Debugging & Tracing Interactionsβ
| Interaction | Description | Example |
|---|---|---|
StartTracing.withScreenshots() | Start trace with screenshots | StartTracing.withScreenshots() |
StartTracing...andSnapshots() | Include DOM snapshots | StartTracing.withScreenshots().andSnapshots() |
StartTracing...andSources() | Include source files | StartTracing.withScreenshots().andSources() |
StartTracing...named(name) | Set trace name | StartTracing.withScreenshots().named("login-test") |
StopTracing.andSaveTo(path) | Stop and save trace | StopTracing.andSaveTo(Paths.get("trace.zip")) |
CaptureConsoleMessages.duringTest() | Start capturing console | CaptureConsoleMessages.duringTest() |
CaptureConsoleMessages.clear() | Clear captured messages | CaptureConsoleMessages.clear() |
CheckConsole.forErrors() | Fail if console errors found | CheckConsole.forErrors() |
CheckConsole.forWarnings() | Fail if console warnings found | CheckConsole.forWarnings() |
CheckConsole.forErrorsAndWarnings() | Fail if errors or warnings found | CheckConsole.forErrorsAndWarnings() |
CheckConsole...andReportOnly() | Report without failing | CheckConsole.forErrors().andReportOnly() |
ReportConsoleMessages.all() | Report all console messages | ReportConsoleMessages.all() |
ReportConsoleMessages.errors() | Report console errors | ReportConsoleMessages.errors() |
ReportConsoleMessages.errorsAndWarnings() | Report errors and warnings | ReportConsoleMessages.errorsAndWarnings() |
CaptureNetworkRequests.duringTest() | Start capturing network | CaptureNetworkRequests.duringTest() |
CaptureNetworkRequests.clear() | Clear captured requests | CaptureNetworkRequests.clear() |
Session State Interactionsβ
| Interaction | Description | Example |
|---|---|---|
SaveSessionState.toPath(path) | Save session to specific path | SaveSessionState.toPath(Paths.get("session.json")) |
SaveSessionState.toFile(name) | Save session to default location | SaveSessionState.toFile("admin-session") |
RestoreSessionState.fromPath(path) | Restore session from path | RestoreSessionState.fromPath(Paths.get("session.json")) |
RestoreSessionState.fromFile(name) | Restore session from default location | RestoreSessionState.fromFile("admin-session") |
API Request Interactionsβ
| Interaction | Description | Example |
|---|---|---|
APIRequest.get(url) | Make GET request | APIRequest.get("https://api.example.com/users") |
APIRequest.post(url) | Make POST request | APIRequest.post("https://api.example.com/users") |
APIRequest.put(url) | Make PUT request | APIRequest.put("https://api.example.com/users/1") |
APIRequest.patch(url) | Make PATCH request | APIRequest.patch("https://api.example.com/users/1") |
APIRequest.delete(url) | Make DELETE request | APIRequest.delete("https://api.example.com/users/1") |
APIRequest.head(url) | Make HEAD request | APIRequest.head("https://api.example.com/users/1") |
APIRequest...withJsonBody(object) | Set JSON request body | APIRequest.post(url).withJsonBody(Map.of("name", "John")) |
APIRequest...withBody(string) | Set string request body | APIRequest.post(url).withBody("<xml>data</xml>") |
APIRequest...withHeader(name, value) | Add request header | APIRequest.get(url).withHeader("Authorization", "Bearer token") |
APIRequest...withQueryParam(name, value) | Add query parameter | APIRequest.get(url).withQueryParam("page", "1") |
APIRequest...withContentType(type) | Set Content-Type | APIRequest.post(url).withContentType("application/xml") |
APIRequest...withTimeout(ms) | Set request timeout | APIRequest.get(url).withTimeout(30000) |
APIRequest...failOnStatusCode(boolean) | Fail on non-2xx status | APIRequest.get(url).failOnStatusCode(true) |
PDF Generation Interactionsβ
| Interaction | Description | Example |
|---|---|---|
GeneratePDF.ofCurrentPage().andSaveTo(path) | Generate PDF | GeneratePDF.ofCurrentPage().andSaveTo(Paths.get("page.pdf")) |
GeneratePDF...withFormat(format) | Set paper format | GeneratePDF.ofCurrentPage().withFormat("A4") |
GeneratePDF...inLandscape() | Use landscape orientation | GeneratePDF.ofCurrentPage().inLandscape() |
GeneratePDF...withMargins(t, r, b, l) | Set margins | GeneratePDF.ofCurrentPage().withMargins("1cm", "1cm", "1cm", "1cm") |
GeneratePDF...printBackground() | Include background graphics | GeneratePDF.ofCurrentPage().printBackground() |
GeneratePDF...displayHeaderAndFooter() | Show header/footer | GeneratePDF.ofCurrentPage().displayHeaderAndFooter() |
Visual Testing Interactionsβ
| Interaction | Description | Example |
|---|---|---|
CompareScreenshot.ofPage().againstBaseline(name) | Compare full page | CompareScreenshot.ofPage().againstBaseline("home.png") |
CompareScreenshot.of(target).againstBaseline(name) | Compare element | CompareScreenshot.of("#card").againstBaseline("card.png") |
CompareScreenshot...withThreshold(threshold) | Set diff threshold | CompareScreenshot.ofPage().againstBaseline("x.png").withThreshold(0.01) |
CompareScreenshot...withMask(targets...) | Mask dynamic elements | CompareScreenshot.ofPage().againstBaseline("x.png").withMask("#time") |
All Questionsβ
The following tables provide a complete reference of all available Playwright Screenplay questions.
Element State Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
Presence.of(target) | Boolean | Element exists in DOM | actor.asksFor(Presence.of("#modal")) |
Absence.of(target) | Boolean | Element not present | actor.asksFor(Absence.of("#error")) |
Visibility.of(target) | Boolean | Element is visible | actor.asksFor(Visibility.of("#popup")) |
Enabled.of(target) | Boolean | Element is enabled | actor.asksFor(Enabled.of("#submit")) |
SelectedStatus.of(target) | Boolean | Checkbox/radio is selected | actor.asksFor(SelectedStatus.of("#agree")) |
Element Content Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
Text.of(target) | String | Get element text content | actor.asksFor(Text.of("#title")) |
Text.ofEach(target) | List<String> | Get text of all matching elements | actor.asksFor(Text.ofEach(".item")) |
Value.of(target) | String | Get input field value | actor.asksFor(Value.of("#email")) |
Attribute.of(target).named(attr) | String | Get attribute value | actor.asksFor(Attribute.of("#link").named("href")) |
CSSValue.of(target).named(prop) | String | Get CSS property value | actor.asksFor(CSSValue.of("#box").named("color")) |
Page Information Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
CurrentUrl.ofThePage() | String | Get current page URL | actor.asksFor(CurrentUrl.ofThePage()) |
PageTitle.ofThePage() | String | Get page title | actor.asksFor(PageTitle.ofThePage()) |
Download Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
DownloadedFile.suggestedFilename() | String | Get suggested filename | actor.asksFor(DownloadedFile.suggestedFilename()) |
DownloadedFile.url() | String | Get download URL | actor.asksFor(DownloadedFile.url()) |
DownloadedFile.path() | Path | Get download file path | actor.asksFor(DownloadedFile.path()) |
DownloadedFile.failure() | String | Get failure reason (null if success) | actor.asksFor(DownloadedFile.failure()) |
DownloadedFile.download() | Download | Get Playwright Download object | actor.asksFor(DownloadedFile.download()) |
Console Message Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
ConsoleMessages.all() | List<String> | Get all console messages | actor.asksFor(ConsoleMessages.all()) |
ConsoleMessages.errors() | List<String> | Get console errors | actor.asksFor(ConsoleMessages.errors()) |
ConsoleMessages.warnings() | List<String> | Get console warnings | actor.asksFor(ConsoleMessages.warnings()) |
ConsoleMessages.logs() | List<String> | Get console logs | actor.asksFor(ConsoleMessages.logs()) |
ConsoleMessages.info() | List<String> | Get console info messages | actor.asksFor(ConsoleMessages.info()) |
ConsoleMessages.containing(text) | List<String> | Get messages containing text | actor.asksFor(ConsoleMessages.containing("error")) |
ConsoleMessages.count() | Integer | Get total message count | actor.asksFor(ConsoleMessages.count()) |
ConsoleMessages.errorCount() | Integer | Get error count | actor.asksFor(ConsoleMessages.errorCount()) |
ConsoleMessages.allCaptured() | List<CapturedConsoleMessage> | Get full message objects | actor.asksFor(ConsoleMessages.allCaptured()) |
Accessibility Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
AccessibilitySnapshot.ofThePage() | String | Get page accessibility tree | actor.asksFor(AccessibilitySnapshot.ofThePage()) |
AccessibilitySnapshot.of(target) | String | Get element accessibility tree | actor.asksFor(AccessibilitySnapshot.of("#nav")) |
AccessibilitySnapshot.allWithRole(role) | List<String> | Get elements by ARIA role | actor.asksFor(AccessibilitySnapshot.allWithRole(AriaRole.BUTTON)) |
Network Request Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
NetworkRequests.all() | List<CapturedRequest> | Get all captured requests | actor.asksFor(NetworkRequests.all()) |
NetworkRequests.count() | Integer | Get total request count | actor.asksFor(NetworkRequests.count()) |
NetworkRequests.withMethod(method) | List<CapturedRequest> | Filter by HTTP method | actor.asksFor(NetworkRequests.withMethod("POST")) |
NetworkRequests.toUrlContaining(text) | List<CapturedRequest> | Filter by URL substring | actor.asksFor(NetworkRequests.toUrlContaining("/api/")) |
NetworkRequests.matching(pattern) | List<CapturedRequest> | Filter by glob pattern | actor.asksFor(NetworkRequests.matching("**/*.js")) |
NetworkRequests.failed() | List<CapturedRequest> | Get failed requests | actor.asksFor(NetworkRequests.failed()) |
NetworkRequests.failedCount() | Integer | Get failed request count | actor.asksFor(NetworkRequests.failedCount()) |
NetworkRequests.clientErrors() | List<CapturedRequest> | Get 4xx errors | actor.asksFor(NetworkRequests.clientErrors()) |
NetworkRequests.serverErrors() | List<CapturedRequest> | Get 5xx errors | actor.asksFor(NetworkRequests.serverErrors()) |
API Response Questionsβ
| Question | Return Type | Description | Example |
|---|---|---|---|
LastAPIResponse.statusCode() | Integer | Get response status code | actor.asksFor(LastAPIResponse.statusCode()) |
LastAPIResponse.ok() | Boolean | Check if status is 2xx | actor.asksFor(LastAPIResponse.ok()) |
LastAPIResponse.body() | String | Get response body as string | actor.asksFor(LastAPIResponse.body()) |
LastAPIResponse.jsonBody() | Map<String, Object> | Parse JSON response as Map | actor.asksFor(LastAPIResponse.jsonBody()) |
LastAPIResponse.jsonBodyAsList() | List<Map<String, Object>> | Parse JSON array response | actor.asksFor(LastAPIResponse.jsonBodyAsList()) |
LastAPIResponse.headers() | Map<String, String> | Get all response headers | actor.asksFor(LastAPIResponse.headers()) |
LastAPIResponse.header(name) | String | Get specific header | actor.asksFor(LastAPIResponse.header("Content-Type")) |
LastAPIResponse.url() | String | Get final URL (after redirects) | actor.asksFor(LastAPIResponse.url()) |
UI Element Factoriesβ
Factory classes for locating common UI elements.
| Factory | Methods | Example |
|---|---|---|
Button | withText(text), withNameOrId(id), withAriaLabel(label), containingText(text), locatedBy(selector) | Button.withText("Submit") |
InputField | withNameOrId(id), withPlaceholder(text), withLabel(label), withAriaLabel(label), locatedBy(selector) | InputField.withPlaceholder("Email") |
Link | withText(text), containingText(text), withTitle(title), locatedBy(selector) | Link.withText("Learn more") |
Checkbox | withLabel(label), withNameOrId(id), withValue(value), locatedBy(selector) | Checkbox.withLabel("I agree") |
RadioButton | withLabel(label), withNameOrId(id), withValue(value), locatedBy(selector) | RadioButton.withValue("express") |
Dropdown | withLabel(label), withNameOrId(id), locatedBy(selector) | Dropdown.withLabel("Country") |
Label | withText(text), withExactText(text), forFieldId(id), locatedBy(selector) | Label.forFieldId("email") |
Image | withAltText(alt), withSrc(src), withSrcContaining(text), locatedBy(selector) | Image.withAltText("Logo") |
Migration from WebDriverβ
When migrating from serenity-screenplay-webdriver:
-
Ability Change:
- Replace
BrowseTheWebwithBrowseTheWebWithPlaywright
- Replace
-
Selector Syntax:
- CSS and XPath work similarly
- Add role selectors for more robust tests
- Use
>>for chaining selectors instead of nesting
-
Waiting:
- Playwright auto-waits; explicit waits are less necessary
- Remove most
WaitUntilcalls
-
Locator Methods:
By.id("x")becomes"#x"By.cssSelector("x")becomes"x"By.xpath("x")becomes"xpath=x"
-
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");
}
}