Saltar al contenido principal

Page Object con Playwright

Los Page Object son un patron de diseno que encapsula la estructura y el comportamiento de una pagina web. Con Playwright y Serenity, los Page Object proporcionan una separacion limpia entre la logica de tus pruebas y los detalles de implementacion de la pagina.

Por que usar Page Object?​

Sin Page Object, las pruebas se vuelven fragiles y dificiles de mantener:

// ❌ Mal: Localizadores dispersos por las pruebas
@Test
void searchTest() {
page.locator("input[name='q']").fill("search term");
page.locator("button.search-btn").click();
assertThat(page.locator(".results-count").textContent()).contains("10");
}

Con Page Object, las pruebas son limpias y mantenibles:

// ✅ Bien: Localizadores encapsulados en Page Object
@Test
void searchTest() {
searchPage.searchFor("search term");
assertThat(searchPage.getResultCount()).isEqualTo(10);
}

Estructura de un Page Object​

Un Page Object bien disenado tiene tres secciones:

public class ProductPage {
private final Page page;

public ProductPage(Page page) {
this.page = page;
}

// ========== LOCALIZADORES (privados) ==========
// Encapsulan como se encuentran los elementos

private Locator productTitle() {
return page.locator("h1.product-title");
}

private Locator addToCartButton() {
return page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to Cart"));
}

private Locator quantityInput() {
return page.locator("#quantity");
}

// ========== ACCIONES (publicas) ==========
// Representan interacciones del usuario

public void addToCart() {
addToCartButton().click();
}

public void setQuantity(int quantity) {
quantityInput().fill(String.valueOf(quantity));
}

// ========== CONSULTAS (publicas) ==========
// Devuelven informacion sobre el estado de la pagina

public String getProductTitle() {
return productTitle().textContent();
}

public boolean isAddToCartEnabled() {
return addToCartButton().isEnabled();
}
}

Principios clave​

  1. Los localizadores son privados - Solo el Page Object sabe como encontrar elementos
  2. Las acciones son publicas - Exponen interacciones significativas del usuario
  3. Las consultas son publicas - Exponen formas de verificar el estado de la pagina
  4. Sin aserciones en Page Object - Deja que quien llama decida que verificar

Estrategias de localizacion en Playwright​

Playwright ofrece varias formas de localizar elementos. Usa la estrategia mas resistente para cada caso:

Localizadores basados en roles (Recomendado)​

Son los mas resistentes ya que usan semantica de accesibilidad:

// Buscar por rol ARIA y nombre accesible
private Locator searchButton() {
return page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Search"));
}

private Locator emailField() {
return page.getByRole(AriaRole.TEXTBOX,
new Page.GetByRoleOptions().setName("Email"));
}

private Locator mainNavigation() {
return page.getByRole(AriaRole.NAVIGATION);
}

Localizadores basados en texto​

Buenos para encontrar elementos por texto visible:

private Locator submitButton() {
return page.getByText("Submit Order");
}

private Locator welcomeMessage() {
return page.getByText("Welcome back", new Page.GetByTextOptions().setExact(false));
}

Localizadores basados en etiquetas​

Encuentran campos de formulario por sus etiquetas asociadas:

private Locator usernameField() {
return page.getByLabel("Username");
}

private Locator rememberMeCheckbox() {
return page.getByLabel("Remember me");
}

Localizadores con Test ID​

Usa atributos data-testid para elementos sin buenos identificadores semanticos:

private Locator shoppingCart() {
return page.getByTestId("shopping-cart");
}

private Locator productCard(String productId) {
return page.getByTestId("product-" + productId);
}

CSS y XPath (Ultimo recurso)​

Usa solo cuando otras estrategias no esten disponibles:

private Locator legacyWidget() {
return page.locator("div.legacy-widget > span.value");
}

private Locator complexElement() {
return page.locator("xpath=//div[@class='container']//span[contains(text(),'Total')]");
}

Patrones de Page Object​

Metodos de navegacion​

Incluye metodos para navegar a la pagina:

public class LoginPage {
private final Page page;
private static final String URL = "/login";

public void open() {
page.navigate(URL);
}

public void openWithRedirect(String returnUrl) {
page.navigate(URL + "?redirect=" + returnUrl);
}
}

Transiciones de pagina​

Devuelve nuevos Page Object cuando las acciones navegan a diferentes paginas:

public class LoginPage {
public DashboardPage loginAs(String username, String password) {
usernameField().fill(username);
passwordField().fill(password);
loginButton().click();
return new DashboardPage(page);
}
}

// Uso:
DashboardPage dashboard = loginPage.loginAs("user", "pass");
assertThat(dashboard.getWelcomeMessage()).contains("Welcome");

Objetos de componente​

Reutiliza componentes de UI comunes entre paginas:

public class HeaderComponent {
private final Page page;

public HeaderComponent(Page page) {
this.page = page;
}

private Locator searchBox() {
return page.locator("header input[type='search']");
}

private Locator cartIcon() {
return page.locator("header .cart-icon");
}

public void search(String term) {
searchBox().fill(term);
searchBox().press("Enter");
}

public int getCartItemCount() {
String text = cartIcon().textContent();
return Integer.parseInt(text);
}
}

// Usar en Page Object:
public class ProductPage {
private final Page page;
private final HeaderComponent header;

public ProductPage(Page page) {
this.page = page;
this.header = new HeaderComponent(page);
}

public HeaderComponent header() {
return header;
}
}

Estrategias de espera​

Playwright espera automaticamente, pero a veces necesitas esperas explicitas:

public class SearchResultsPage {
public void waitForResults() {
// Esperar a que los resultados carguen
resultsContainer().waitFor();
}

public void waitForMinimumResults(int count) {
// Esperar hasta tener suficientes resultados
page.waitForCondition(() -> getResultCount() >= count);
}

public void waitForLoadingToComplete() {
// Esperar a que el spinner de carga desaparezca
loadingSpinner().waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.HIDDEN));
}
}

Integracion con Step Library​

Los Page Object funcionan junto con las Step Library. La Step Library maneja los reportes de Serenity mientras que el Page Object maneja las interacciones con la pagina:

public class CheckoutSteps {
private CheckoutPage checkoutPage;
private OrderConfirmationPage confirmationPage;

@Step("Enter shipping address")
public void enterShippingAddress(Address address) {
checkoutPage.enterAddress(address);
}

@Step("Complete the order")
public void completeOrder() {
confirmationPage = checkoutPage.submitOrder();
}

@Step("Verify order confirmation number is displayed")
public void verifyOrderConfirmationDisplayed() {
assertThat(confirmationPage.getOrderNumber())
.isNotEmpty();
}
}

Mejores practicas​

1. Un Page Object por pagina/vista​

No intentes representar multiples paginas en una clase:

// ❌ Mal
public class AllPages { /* demasiadas responsabilidades */ }

// ✅ Bien
public class LoginPage { /* solo login */ }
public class DashboardPage { /* solo dashboard */ }

2. Usa nombres de metodos descriptivos​

Los nombres de los metodos deben describir la intencion del usuario:

// ❌ Mal
public void click1() { ... }
public void fillForm() { ... }

// ✅ Bien
public void addToWishlist() { ... }
public void submitContactForm(String name, String email, String message) { ... }

3. Evita exponer localizadores​

Nunca devuelvas objetos Locator crudos:

// ❌ Mal - expone implementacion
public Locator getSubmitButton() {
return page.locator("#submit");
}

// ✅ Bien - expone comportamiento
public void submitForm() {
submitButton().click();
}

public boolean isSubmitEnabled() {
return submitButton().isEnabled();
}

4. Maneja contenido dinamico​

Usa localizadores parametrizados para elementos dinamicos:

public class ProductListPage {
public void selectProduct(String productName) {
page.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName(productName)).click();
}

public String getPriceFor(String productName) {
return page.locator(String.format(
"[data-product='%s'] .price", productName)).textContent();
}
}