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:
LoginPage— goto(), login(), loginWithGoogle(), errorMessageProductListPage— searchProduct(), filterByCategory(), addToCart(), getProductCount()CartPage— getItems(), updateQuantity(), removeItem(), getTotal(), proceedToCheckout()CheckoutPage— enterShipping(), enterPayment(), placeOrder()OrderConfirmationPage— getOrderNumber(), getTotal(), getEstimatedDelivery()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