Page Objects: Imports and Idioms
Verified against Serenity 5.4.1 (Playwright module: 5.3.11, the latest published). The examples at the end of this page compile and their tests pass.
Page Objects with step libraries work with Selenium and with Playwright, with JUnit 5. This is as valid as Screenplay. Use the style the user asked for or the project already uses, and don't mix Page Objects and Screenplay in one test.
1. Structure
Test (@Steps field) -> Step library (@Step methods) -> Page Object (locators and page actions)
Tests call step methods that read like business actions. Page Objects hide locators.
2. Where things live
| Class | Package |
|---|---|
@Step, @Steps, @Managed, @DefaultUrl, @WhenPageOpens, @WithTag, @Title, @Narrative, @Pending | net.serenitybdd.annotations (never net.thucydides.core.annotations) |
SerenityJUnit5Extension | net.serenitybdd.junit5 |
PageObject, WebElementFacade | net.serenitybdd.core.pages |
UIInteractionSteps (base class for Selenium step libraries) | net.serenitybdd.core.steps |
Serenity (static facade) | net.serenitybdd.core.Serenity |
ScenarioSteps (older step library base class, still valid) | net.thucydides.core.steps |
Pages | net.thucydides.core.pages |
@FindBy | org.openqa.selenium.support.FindBy (Serenity also has net.serenitybdd.core.annotations.findby.FindBy) |
net.thucydides.core.pages.WebElementFacade and net.thucydides.core.annotations.findby.FindBy are deprecated aliases. Use the net.serenitybdd ones.
Maven: Selenium needs serenity-core and serenity-junit5. Playwright adds serenity-playwright and com.microsoft.playwright:playwright.
3. Selenium Page Objects
Files: po-selenium/todo/{TodoPage,TodoSteps,AddTodoTest}.java
- Page:
@DefaultUrl("https://..."),extends PageObject,@FindBy(css = "...") WebElementFacade field;, or$("css"),$$("css")for lookups. A String that starts with a tag name is not read as CSS.$("button"),$("th[data-sort='title']")andthenFind("button")compile as XPath, so they look for the wrong thing and fail withElementShouldBeEnabledExceptionorNoSuchElementException ... xpath. Start the selector with#or.($("#go"),$(".sort")), or use$(By.cssSelector("button"))(for a child elementrow.findBy(By.cssSelector("button"));thenFindhas noByversion), or write a real XPath. The same rule applies to ScreenplayTarget.the("name").locatedBy(..). - Don't write your own
open().PageObject.open()already exists and opens the@DefaultUrl; it isfinal, so redeclaring it fails to compile. For another URL useopenAt("https://..."), or a differently named method. @FindBy(css = ...)takes a String.$(By.cssSelector(..))takes aBy. Don't mix them up.- Step library:
extends UIInteractionSteps, a field for each page object (injected automatically), and@Step("Add the todo '{0}'")on public methods.{0}is the first argument. - Test:
@ExtendWith(SerenityJUnit5Extension.class),@Managed(driver = "chrome") WebDriver driver;,@Steps TodoSteps todo;. Assertions in the test with AssertJ or Hamcrest, or withassertThatinside a@Stepmethod. - Useful
WebElementFacademethods:type(..),typeAndEnter(..),click(),getText(),isVisible(),waitUntilVisible(),selectByVisibleText(..),getValue().
4. Playwright Page Objects
Files: po-playwright/todo/{TodoPage,TodoSteps,AddTodoTest}.java
- The Page Object is a plain class that holds a
com.microsoft.playwright.Page. It does not extendPageObject, and doesn't use@FindBy,WebElementFacadeor@DefaultUrl. Locators are Playwrightpage.locator("css"),getByRole(..). - A locator used with
click(),fill(),textContent()orwaitFor()must match exactly one element (strict mode); for lists use.first(),.nth(i),.count()orallTextContents(). Don't build a Playwright text pattern withPattern.quote(..). Playwright sends the pattern to the browser's JavaScript engine, which doesn't understand Java's\Q..\E, sosetHasText(Pattern.compile("^\\s*" + Pattern.quote(title) + "\\s*$"))matches nothing and the step times out after 30 seconds (two Opus runs). Use the plain string (setHasText(title)matches by substring, ignoring case;text-is('..')/has-text('..')in a selector string match exactly or by substring), or write the pattern by hand (Pattern.compile("^\\s*Clean Code\\s*$")). Run in the verification script in the serenity-core repository (docs/ai/skill). - Playwright Java API details models get wrong (run in the verification script in the serenity-core repository (
docs/ai/skill)):page.getByRole(role, new Page.GetByRoleOptions()...)butlocator.getByRole(role, new Locator.GetByRoleOptions()...): passing thePageoptions to a locator is a compile error; and as a selector string a role needs the prefix,"role=heading[name=\"Sign in\"]":"heading[name=...]"throwsInvalidSelectorError. - The test owns the browser: start
Playwrightand theBrowserin@BeforeAll, and in@BeforeEachcreate a page and callPlaywrightSerenity.registerPage(page). In@AfterEachcallPlaywrightSerenity.unregisterPage(page)and close the page. - Step library: a plain class with
@Stepmethods that get the page fromPlaywrightSerenity.getCurrentPage()(or take aPageparameter) and wrap it in the Page Object. NoUIInteractionSteps. PlaywrightSerenityis innet.serenitybdd.playwright. A@SerenityPlaywrightmeta-annotation (net.serenitybdd.playwright.junit5) combines the extensions. Seeserenity-playwright-demofor it.
5. Common mistakes
| Wrong | Right |
|---|---|
import net.thucydides.core.annotations.{Step,Steps,Managed,DefaultUrl} (most Page Object failures) | net.serenitybdd.annotations.* |
Declaring public void open() in a page object | Delete it. Use the inherited open() with @DefaultUrl, or openAt(url) |
@FindBy("...") or passing a String where a By is needed (and the reverse) | @FindBy(css = "..."); $("css") for Strings; $(By.id("x")) for a By |
$("button"), $("th[data-sort='title']"), row.thenFind("button") (read as XPath) | $("#go"), $(By.cssSelector("button")), row.findBy(By.cssSelector("button")) |
JUnit 4 @RunWith(SerenityRunner.class) | @ExtendWith(SerenityJUnit5Extension.class) |
@Page annotation | Doesn't exist. Use a page object field in the step library, or @Steps for step libraries in the test |
ScenarioSteps from net.serenitybdd... | net.thucydides.core.steps.ScenarioSteps (this one is under net.thucydides) |
Extending PageObject for a Playwright page, or Page from Selenium | Playwright pages are plain classes using com.microsoft.playwright.Page |
6. Not yet verified
- Runtime behaviour beyond the examples: they ran once against the live todo app.
@WhenPageOpens,waitFor/waitUntil*helpers andPages-based injection are not covered by examples.- The Playwright section is based on 5.3.11 and the
serenity-playwright-demomodule.
Examples
These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11).
po-selenium/todo/AddTodoTest.java
package todo;
import net.serenitybdd.annotations.Managed;
import net.serenitybdd.annotations.Steps;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SerenityJUnit5Extension.class)
class AddTodoTest {
@Managed(driver = "chrome")
WebDriver driver;
@Steps
TodoSteps todo;
@Test
void shouldShowTheNewTodo() {
todo.openTheApplication();
todo.addTodo("Buy milk");
assertThat(todo.todoItems()).contains("Buy milk");
}
}
po-selenium/todo/TodoPage.java
package todo;
import net.serenitybdd.annotations.DefaultUrl;
import net.serenitybdd.core.pages.PageObject;
import net.serenitybdd.core.pages.WebElementFacade;
import org.openqa.selenium.support.FindBy;
import java.util.List;
// Do not declare your own open(): PageObject already has open() and it uses @DefaultUrl.
@DefaultUrl("https://todo-app.serenity-js.org")
public class TodoPage extends PageObject {
@FindBy(css = ".new-todo")
WebElementFacade newTodoField;
public void addTodo(String item) {
newTodoField.typeAndEnter(item);
}
public List<String> todoItems() {
return $$(".todo-list li label").stream().map(WebElementFacade::getText).toList();
}
}
po-selenium/todo/TodoSteps.java
package todo;
import net.serenitybdd.annotations.Step;
import net.serenitybdd.core.steps.UIInteractionSteps;
import java.util.List;
public class TodoSteps extends UIInteractionSteps {
TodoPage todoPage; // page objects are injected into step libraries
@Step("Open the todo application")
public void openTheApplication() {
todoPage.open();
}
@Step("Add the todo '{0}'")
public void addTodo(String item) {
todoPage.addTodo(item);
}
public List<String> todoItems() {
return todoPage.todoItems();
}
}
po-playwright/todo/AddTodoTest.java
package todo;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import net.serenitybdd.annotations.Steps;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.playwright.PlaywrightSerenity;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SerenityJUnit5Extension.class)
class AddTodoTest {
@Steps
TodoSteps todo;
static Playwright playwright;
static Browser browser;
Page page;
@BeforeAll
static void startBrowser() {
playwright = Playwright.create();
browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
}
@AfterAll
static void stopBrowser() {
browser.close();
playwright.close();
}
@BeforeEach
void openPage() {
page = browser.newPage();
PlaywrightSerenity.registerPage(page); // lets Serenity report screenshots and steps
}
@AfterEach
void closePage() {
PlaywrightSerenity.unregisterPage(page);
page.close();
}
@Test
void shouldShowTheNewTodo() {
todo.openTheApplication();
todo.addTodo("Buy milk");
assertThat(todo.todoItems()).contains("Buy milk");
}
}
po-playwright/todo/TodoPage.java
package todo;
import com.microsoft.playwright.Page;
import java.util.List;
/** A plain class that wraps a Playwright Page. It does not extend Serenity's PageObject. */
public class TodoPage {
private final Page page;
public TodoPage(Page page) {
this.page = page;
}
public void open() {
page.navigate("https://todo-app.serenity-js.org");
}
public void addTodo(String item) {
page.locator(".new-todo").fill(item);
page.locator(".new-todo").press("Enter");
}
public List<String> todoItems() {
return page.locator(".todo-list li label").allInnerTexts();
}
}
po-playwright/todo/TodoSteps.java
package todo;
import com.microsoft.playwright.Page;
import net.serenitybdd.annotations.Step;
import net.serenitybdd.playwright.PlaywrightSerenity;
import java.util.List;
public class TodoSteps {
private TodoPage todoPage() {
Page page = PlaywrightSerenity.getCurrentPage(); // the page registered by the test
return new TodoPage(page);
}
@Step("Open the todo application")
public void openTheApplication() {
todoPage().open();
}
@Step("Add the todo '{0}'")
public void addTodo(String item) {
todoPage().addTodo(item);
}
public List<String> todoItems() {
return todoPage().todoItems();
}
}