6.0 KiB
6.0 KiB
Playwright Java – Page Object Patterns
Component Pattern (Reusable Sub-Page-Objects)
For repeated UI components (navbars, modals, tables), create Component classes:
// Reusable table component — pass the locator root
public class DataTable extends BasePage {
private final Locator tableRoot;
public DataTable(Page page, Locator tableRoot) {
super(page);
this.tableRoot = tableRoot;
}
public int getRowCount() {
return tableRoot.locator("tbody tr").count();
}
public String getCellValue(int row, int col) {
return tableRoot.locator("tbody tr")
.nth(row)
.locator("td")
.nth(col)
.innerText();
}
public void clickRowAction(int row, String actionLabel) {
tableRoot.locator("tbody tr")
.nth(row)
.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName(actionLabel))
.click();
}
public DataTable sortByColumn(String columnHeader) {
tableRoot.getByRole(AriaRole.COLUMNHEADER,
new Locator.GetByRoleOptions().setName(columnHeader)).click();
return this;
}
}
// Usage in a page object:
public class UsersPage extends BasePage {
public final DataTable usersTable;
private final Locator searchInput;
private final Locator addUserButton;
public UsersPage(Page page) {
super(page);
usersTable = new DataTable(page, page.locator("#users-table"));
searchInput = page.getByPlaceholder("Search users…");
addUserButton = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Add User"));
}
@Override protected String getUrl() { return "/admin/users"; }
public UsersPage searchFor(String query) {
searchInput.fill(query);
searchInput.press("Enter");
page.waitForResponse("**/api/users**", () -> {});
return this;
}
}
Modal / Dialog Component
public class ConfirmDialog extends BasePage {
private final Locator dialog;
private final Locator confirmButton;
private final Locator cancelButton;
private final Locator titleText;
public ConfirmDialog(Page page) {
super(page);
dialog = page.getByRole(AriaRole.DIALOG);
confirmButton = dialog.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Confirm"));
cancelButton = dialog.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Cancel"));
titleText = dialog.getByRole(AriaRole.HEADING);
}
public void waitForOpen() {
dialog.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE));
}
public void confirm() { confirmButton.click(); dialog.waitFor(
new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN)); }
public void cancel() { cancelButton.click(); }
public String getTitle() { return titleText.innerText(); }
@Override protected String getUrl() { return ""; }
}
Navigation Chain Pattern
Page methods should return the next page object — never return void for navigating actions:
// CORRECT — enables fluent chaining
public DashboardPage loginAs(String email, String password) {
fill(emailInput, email);
fill(passwordInput, password);
clickAndWaitForNav(submitButton);
return new DashboardPage(page);
}
// USAGE
DashboardPage dash = new LoginPage(page)
.navigate()
.loginAs("user@test.com", "secret")
.navigateTo(OrdersPage::new) // helper to reduce boilerplate
.filterByStatus("PENDING");
Dynamic Locators
For lists of items that share structure:
// Locate a card by its title
public Locator getProductCard(String productName) {
return page.locator(".product-card")
.filter(new Locator.FilterOptions().setHasText(productName));
}
// Wait for a specific row to appear in a table
public void waitForOrderRow(String orderId) {
page.locator("tr[data-order-id='" + orderId + "']")
.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE)
.setTimeout(15_000));
}
Dropdown / Select Handling
// Native <select>
page.selectOption("#country-select", "India");
// Custom dropdown (non-native)
public void selectCountry(String countryName) {
page.getByLabel("Country").click();
page.getByRole(AriaRole.LISTBOX)
.getByText(countryName)
.click();
}
File Upload
// Standard file input
page.setInputFiles("#file-upload", Paths.get("src/test/resources/testdata/sample.pdf"));
// Drag-and-drop upload zone
page.locator(".upload-zone").setInputFiles(Paths.get("src/test/resources/testdata/sample.pdf"));
// Multiple files
page.setInputFiles("#file-upload", new Path[]{
Paths.get("file1.png"),
Paths.get("file2.png")
});
Download Handling
Download download = page.waitForDownload(() ->
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Export CSV")).click()
);
Path downloadedFile = download.path();
assertThat(downloadedFile.toFile()).exists();
Hover & Tooltip Verification
page.getByTestId("info-icon").hover();
Locator tooltip = page.getByRole(AriaRole.TOOLTIP);
tooltip.waitFor();
assertThat(tooltip).hasText("This field is required");
Waiting Strategies (Anti-Flake)
// Wait for API response after action
page.waitForResponse(resp -> resp.url().contains("/api/search") && resp.status() == 200,
() -> searchInput.fill("test"));
// Wait for network idle (after complex renders)
page.waitForLoadState(LoadState.NETWORKIDLE);
// Wait for element count to stabilize
Locator rows = page.locator("tbody tr");
rows.first().waitFor(); // wait for at least one
assertThat(rows).hasCount(10);
// Polling custom condition
Assertions.assertDoesNotThrow(() -> {
page.waitForCondition(() -> page.locator(".spinner").count() == 0);
});