Que Es el Page Object Model?

El Page Object Model (POM) es el patron de diseno mas utilizado en automatizacion de UI. Crea una clase para cada pagina o componente de tu aplicacion, encapsulando todas las interacciones con esa pagina.

Sin POM — selectores y acciones dispersos en los tests:

test('usuario puede hacer login', async ({ page }) => {
  await page.fill('#email', 'admin@test.com');
  await page.fill('#password', 'secret');
  await page.click('button.login-btn');
  await expect(page.locator('.welcome-msg')).toHaveText('Bienvenido, Admin');
});

Con POM — detalles de pagina encapsulados:

test('usuario puede hacer login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('admin@test.com', 'secret');
  const dashboard = new DashboardPage(page);
  await expect(dashboard.welcomeMessage).toHaveText('Bienvenido, Admin');
});

Principios Fundamentales

1. Una Clase por Pagina (o Componente)

Cada pagina de tu aplicacion tiene su propia clase:

pages/
  LoginPage.ts
  DashboardPage.ts
  ProductListPage.ts
  ProductDetailPage.ts
  CheckoutPage.ts
  CartPage.ts

2. Los Selectores Son Privados

Los selectores son detalles de implementacion que no deben filtrarse a los tests:

class LoginPage {
  #emailInput = '[data-testid="email"]';
  #passwordInput = '[data-testid="password"]';
  #submitBtn = '[data-testid="login-submit"]';
  #errorMessage = '.login-error';

  constructor(page) {
    this.page = page;
  }
}

3. Los Metodos Publicos Representan Acciones del Usuario

Los metodos deben nombrarse segun lo que hace el usuario, no lo que hace el codigo:

class LoginPage {
  // Bien — nombres centrados en el usuario
  async login(email, password) { /* ... */ }
  async forgotPassword(email) { /* ... */ }
  async loginWithGoogle() { /* ... */ }

  // Mal — nombres centrados en implementacion
  async fillEmailAndClickSubmit() { /* ... */ }
  async clickGoogleButton() { /* ... */ }
}

4. Sin Aserciones en Page Objects

Los page objects realizan acciones y retornan datos. Los tests hacen aserciones:

// Page object — retorna datos, sin aserciones
class DashboardPage {
  get welcomeMessage() {
    return this.page.locator('.welcome-msg');
  }

  async getUserName() {
    return await this.page.textContent('.user-name');
  }

  async getNotificationCount() {
    const text = await this.page.textContent('.notification-badge');
    return parseInt(text);
  }
}

// Test — hace aserciones
test('dashboard muestra nombre de usuario', async ({ page }) => {
  const dashboard = new DashboardPage(page);
  await expect(dashboard.welcomeMessage).toContainText('Admin');
  const count = await dashboard.getNotificationCount();
  expect(count).toBeGreaterThan(0);
});

Ejemplo Completo de Page Object

export class LoginPage {
  #emailInput = '[data-testid="email"]';
  #passwordInput = '[data-testid="password"]';
  #submitBtn = '[data-testid="login-submit"]';
  #errorMessage = '[data-testid="login-error"]';
  #googleOAuth = '[data-testid="google-login"]';

  constructor(page) {
    this.page = page;
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email, password) {
    await this.page.fill(this.#emailInput, email);
    await this.page.fill(this.#passwordInput, password);
    await this.page.click(this.#submitBtn);
  }

  async loginWithGoogle() {
    await this.page.click(this.#googleOAuth);
  }

  get errorMessage() {
    return this.page.locator(this.#errorMessage);
  }

  async isLoaded() {
    await this.page.waitForSelector(this.#emailInput);
  }
}

Method Chaining

El method chaining permite sintaxis fluida retornando page objects desde metodos de navegacion:

class LoginPage {
  async login(email, password) {
    await this.page.fill(this.#emailInput, email);
    await this.page.fill(this.#passwordInput, password);
    await this.page.click(this.#submitBtn);
    return new DashboardPage(this.page);
  }
}

class DashboardPage {
  async navigateToProfile() {
    await this.page.click('#profile-link');
    return new ProfilePage(this.page);
  }
}

// Test fluido
test('usuario navega al perfil', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  const dashboard = await loginPage.login('admin@test.com', 'secret');
  const profile = await dashboard.navigateToProfile();
  await expect(profile.nameField).toHaveValue('Admin User');
});

Component Objects

Para elementos que aparecen en multiples paginas (barra de navegacion, footer, modales), crea component objects:

class NavBar {
  constructor(page) {
    this.page = page;
  }

  async searchFor(query) {
    await this.page.fill('#search', query);
    await this.page.press('#search', 'Enter');
  }

  async navigateTo(section) {
    await this.page.click(`nav a[href="/${section}"]`);
  }

  async logout() {
    await this.page.click('#user-menu');
    await this.page.click('#logout');
  }
}

// Componer en page objects
class DashboardPage {
  constructor(page) {
    this.page = page;
    this.navBar = new NavBar(page);
  }
}

Patrones Avanzados de POM

Clase Base de Pagina

class BasePage {
  constructor(page) {
    this.page = page;
    this.navBar = new NavBar(page);
    this.footer = new Footer(page);
  }

  async goto(path) {
    await this.page.goto(path);
    await this.waitForPageLoad();
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('domcontentloaded');
  }

  async getPageTitle() {
    return await this.page.title();
  }

  async takeScreenshot(name) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }
}

Manejando Elementos Dinamicos

class ProductListPage extends BasePage {
  productCard(name) {
    return this.page.locator(`.product-card:has-text("${name}")`);
  }

  async addToCart(productName) {
    const card = this.productCard(productName);
    await card.locator('.add-to-cart-btn').click();
  }

  async getPrice(productName) {
    const card = this.productCard(productName);
    const priceText = await card.locator('.price').textContent();
    return parseFloat(priceText.replace('$', ''));
  }

  async getProductCount() {
    return await this.page.locator('.product-card').count();
  }
}

Manejando Waits y Estados de Carga

class CheckoutPage extends BasePage {
  async submitPayment(cardNumber) {
    await this.page.fill('#card-number', cardNumber);
    await this.page.fill('#expiry', '12/28');
    await this.page.fill('#cvv', '123');

    const [response] = await Promise.all([
      this.page.waitForResponse(resp =>
        resp.url().includes('/api/payment') && resp.status() === 200
      ),
      this.page.click('#pay-button')
    ]);

    return new OrderConfirmationPage(this.page);
  }
}

POM con Playwright Fixtures

Playwright soporta POM nativamente a traves de fixtures:

import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';

export const test = base.extend({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  }
});

test('usuario ve dashboard', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('admin@test.com', 'secret');
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

Anti-Patrones de POM

1. Page Object Dios

Un page object con 50+ metodos es demasiado grande. Divide en objetos mas pequenos y enfocados.

2. Aserciones en Page Objects

Mantiene las aserciones en tests. Los page objects retornan datos; los tests verifican.

3. Exponer Selectores

Los tests nunca deben referenciar selectores directamente.

4. Estado Mutable Compartido

Los page objects no deben almacenar estado especifico de test.

Ejercicio: Construye un POM Completo

Crea page objects para un flujo de checkout e-commerce:

  1. LoginPage — goto(), login(), loginWithGoogle(), errorMessage
  2. ProductListPage — searchProduct(), filterByCategory(), addToCart(), getProductCount()
  3. CartPage — getItems(), updateQuantity(), removeItem(), getTotal(), proceedToCheckout()
  4. CheckoutPage — enterShipping(), enterPayment(), placeOrder()
  5. OrderConfirmationPage — getOrderNumber(), getTotal(), getEstimatedDelivery()
  6. NavBar (componente) — search(), navigateTo(), cartCount(), logout()

Escribe 3 test cases que usen estos page objects.

Puntos Clave

  • POM separa detalles de pagina de logica de test — un punto de actualizacion por cambio de UI
  • Mantiene selectores privados, expone metodos de acciones del usuario
  • Sin aserciones en page objects — retorna datos, deja que los tests verifiquen
  • Usa component objects para elementos compartidos (nav, footer, modales)
  • Method chaining hace los tests fluidos y legibles
  • Playwright fixtures se integran naturalmente con POM