davila7--claude-code-templates
225 行
6.0 KiB
Markdown
225 行
6.0 KiB
Markdown
# Playwright Java – Page Object Patterns
|
|
|
|
## Component Pattern (Reusable Sub-Page-Objects)
|
|
|
|
For repeated UI components (navbars, modals, tables), create Component classes:
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
page.getByTestId("info-icon").hover();
|
|
Locator tooltip = page.getByRole(AriaRole.TOOLTIP);
|
|
tooltip.waitFor();
|
|
assertThat(tooltip).hasText("This field is required");
|
|
```
|
|
|
|
---
|
|
|
|
## Waiting Strategies (Anti-Flake)
|
|
|
|
```java
|
|
// 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);
|
|
});
|
|
```
|