El Page Object Model es clases de JavaScript aplicadas a la automatización de navegadores: una clase LoginPage tiene un constructor que almacena locators, métodos que encapsulan interacciones, y hereda la navegación común de una BasePage mediante extends. El error más común cuando se aprende este patrón es olvidar super() como primera línea del constructor de la clase hija, lo que impide que se configuren los locators del padre. Esta guía cubre constructores, this, extends, super y métodos estáticos de utilidad, construyendo hasta un ejemplo completo de page object.
Qué es una clase
Una clase es un plano para crear objetos. Cada LoginPage que creas a partir de la clase obtiene los mismos métodos pero opera de forma independiente.
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
}
async navigate() {
await this.page.goto('/login');
}
async login(email, password) {
await this.navigate();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Luego la usas así:
const loginPage = new LoginPage(page);
await loginPage.login('usuario@test.com', 'PassValida1');El constructor
El constructor se ejecuta cuando creas una nueva instancia con new. Es donde inicializas las propiedades.
class ProductPage {
constructor(page) {
// 'this' hace referencia a la instancia que se está creando
this.page = page;
this.productTitle = page.getByTestId('product-title');
this.addToCartButton = page.getByTestId('add-to-cart');
this.price = page.getByTestId('product-price');
this.baseURL = '/products';
}
}
// Cuando llamás new ProductPage(page):
// - el constructor corre inmediatamente
// - 'this.page' se asigna al argumento page
// - los locators se almacenan en la instancia
const productPage = new ProductPage(page);this en las clases
this hace referencia a la instancia actual. Es cómo los métodos acceden a los datos de la instancia.
class DashboardPage {
constructor(page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome');
this.userName = page.getByTestId('user-name');
this.logoutButton = page.getByTestId('logout');
}
async getUserName() {
// 'this.userName' es el locator almacenado en el constructor
return await this.userName.textContent();
}
async logout() {
// 'this.logoutButton' es el locator del botón
await this.logoutButton.click();
}
async verifyUser(nombreEsperado) {
// Los métodos pueden llamar a otros métodos en 'this'
const nombre = await this.getUserName();
if (nombre !== nombreEsperado) {
throw new Error(`Se esperaba ${nombreEsperado}, se obtuvo ${nombre}`);
}
}
}Métodos
Los métodos son funciones definidas en una clase. Pueden ser síncronos o async.
class SearchPage {
constructor(page) {
this.page = page;
this.searchInput = page.getByTestId('search-input');
this.searchButton = page.getByTestId('search-button');
this.results = page.getByTestId('search-result');
this.noResultsMessage = page.getByTestId('no-results');
}
// Método async simple
async search(query) {
await this.searchInput.fill(query);
await this.searchButton.click();
}
// Método que devuelve un valor
async getResultCount() {
return await this.results.count();
}
// Método con múltiples pasos
async searchAndVerify(query, countEsperado) {
await this.search(query);
await this.page.waitForLoadState('networkidle');
const count = await this.getResultCount();
return count === countEsperado;
}
}Herencia con extends
La herencia permite que una clase se construya sobre otra. En POM, generalmente tienes una BasePage que todas las páginas extienden.
// Clase base con comportamiento común
class BasePage {
constructor(page) {
this.page = page;
this.header = page.getByTestId('header');
this.footer = page.getByTestId('footer');
}
async navigate(path) {
await this.page.goto(path);
}
async getTitle() {
return await this.page.title();
}
async takeScreenshot(nombre) {
await this.page.screenshot({ path: `screenshots/${nombre}.png` });
}
}
// La clase hija hereda todo de BasePage
class LoginPage extends BasePage {
constructor(page) {
super(page); // Debe llamarse primero — ejecuta el constructor de BasePage
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
this.errorMessage = page.getByTestId('error-message');
}
async login(email, password) {
await this.navigate('/login'); // Heredado de BasePage
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorText() {
return await this.errorMessage.textContent();
}
}Ahora LoginPage tiene sus propios métodos Y todo lo de BasePage:
const loginPage = new LoginPage(page);
await loginPage.login('usuario@test.com', 'pass'); // método de LoginPage
await loginPage.takeScreenshot('despues-login'); // método de BasePage
const titulo = await loginPage.getTitle(); // método de BasePagesuper: llamar al padre
super() llama al constructor de la clase padre. Es obligatorio como primera línea en el constructor de una clase hija.
class AdminPage extends BasePage {
constructor(page) {
super(page); // Ejecuta el constructor de BasePage: asigna this.page, this.header, this.footer
// Ahora agrega las cosas específicas de AdminPage
this.userTable = page.getByTestId('users-table');
this.addUserButton = page.getByTestId('add-user');
}
}También puedes llamar a métodos del padre con super.nombreMetodo():
class CheckoutPage extends BasePage {
async navigate() {
// Llamar al navigate del padre, luego hacer cosas extra
await super.navigate('/checkout');
await this.page.waitForSelector('[data-testid="checkout-form"]');
}
}Métodos estáticos
Los métodos estáticos pertenecen a la clase en sí, no a las instancias. Son útiles para funciones de utilidad.
class TestData {
static generateEmail() {
return `user_${Date.now()}@test.com`;
}
static generateUser() {
return {
email: TestData.generateEmail(),
password: 'PassValida1',
name: 'Usuario Test',
};
}
static generateProduct(overrides = {}) {
return {
name: 'Producto Test',
price: 99.99,
category: 'electronica',
...overrides,
};
}
}
// Usar sin 'new': llamar directamente en la clase
const email = TestData.generateEmail();
const usuario = TestData.generateUser();
const producto = TestData.generateProduct({ price: 49.99 });Getters
Los getters parecen propiedades pero ejecutan una función cuando se accede a ellos:
class CartPage extends BasePage {
constructor(page) {
super(page);
this.items = page.getByTestId('cart-item');
this.totalElement = page.getByTestId('cart-total');
}
get url() {
return '/carrito'; // Propiedad calculada
}
async navigate() {
await super.navigate(this.url); // Usa el getter
}
async getItemCount() {
return await this.items.count();
}
}
const cartPage = new CartPage(page);
console.log(cartPage.url); // '/carrito' — sin () necesariosUn ejemplo completo de page object
Todo junto, un page object realista:
class ProductListPage extends BasePage {
constructor(page) {
super(page);
this.productCards = page.getByTestId('product-card');
this.filterDropdown = page.getByTestId('category-filter');
this.sortDropdown = page.getByTestId('sort-select');
this.searchInput = page.getByTestId('product-search');
this.loadingSpinner = page.getByTestId('loading');
}
async navigate() {
await super.navigate('/productos');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async filterByCategory(category) {
await this.filterDropdown.selectOption(category);
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async sortBy(sortOption) {
await this.sortDropdown.selectOption(sortOption);
}
async search(query) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async getProductCount() {
return await this.productCards.count();
}
async getProductNames() {
return await this.productCards.getByTestId('product-name').allTextContents();
}
async clickProduct(index) {
await this.productCards.nth(index).click();
}
}// En los tests
test('filtrar productos por categoría', async ({ page }) => {
const productos = new ProductListPage(page);
await productos.navigate();
await productos.filterByCategory('electronica');
const count = await productos.getProductCount();
expect(count).toBeGreaterThan(0);
});Clase vs. objeto literal
Para algo de uso único, un objeto literal es más simple. Para algo que reutilizas en varios tests, una clase tiene sentido.
// Objeto literal: bien para constantes
const TEST_USERS = {
admin: { email: 'admin@test.com', password: 'AdminPass1' },
member: { email: 'member@test.com', password: 'MemberPass1' },
};
// Clase: buena para page objects usados en muchos tests
class LoginPage extends BasePage {
// ...
}Resumen
constructor(page): corre connew, almacena los locators acáthis: hace referencia a la instancia; cómo los métodos acceden a los datos del objetoextends: heredar de otra clase (por ejemplo,BasePage)super(): llama al constructor del padre; es la primera línea obligatoria en el constructor de la clase hijastatic: métodos a nivel de clase, sin necesidad de instancia- Los métodos pueden ser async: devuelven promesas, usa
awaitdentro