Files
antigravity-skills-reference/skills/playwright-java/page-objects.md

6.0 KiB
Raw Blame History

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);
});