O Page Object Model é classes JavaScript aplicadas à automação de navegador. Uma classe LoginPage tem um construtor que armazena locators, métodos que encapsulam interações e herda navegação comum de uma BasePage via extends. O erro mais comum ao aprender esse padrão é esquecer super() como primeira linha do construtor filho, o que impede que os locators da classe pai sejam configurados.

O que é uma classe?

Uma classe é um blueprint para criar objetos. Toda LoginPage criada a partir da classe recebe os mesmos métodos, mas opera de forma independente.

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, senha) {
    await this.navigate();
    await this.emailInput.fill(email);
    await this.passwordInput.fill(senha);
    await this.submitButton.click();
  }
}

Depois você usa assim:

const loginPage = new LoginPage(page);
await loginPage.login('usuario@teste.com', 'SenhaValida1');

O constructor

O construtor roda quando você cria uma nova instância com new. É onde você inicializa as propriedades.

class ProductPage {
  constructor(page) {
    // 'this' se refere à instância sendo criada
    this.page = page;
    this.tituloProduto = page.getByTestId('product-title');
    this.botaoAdicionarCarrinho = page.getByTestId('add-to-cart');
    this.preco = page.getByTestId('product-price');
    this.baseURL = '/products';
  }
}

// Quando você chama new ProductPage(page):
// - o constructor roda imediatamente
// - 'this.page' recebe o argumento page
// - os locators são armazenados na instância
const productPage = new ProductPage(page);

this nas classes

this se refere à instância atual. É como os métodos acessam os dados da instância.

class DashboardPage {
  constructor(page) {
    this.page = page;
    this.mensagemBemVindo = page.getByTestId('welcome');
    this.nomeUsuario = page.getByTestId('user-name');
    this.botaoLogout = page.getByTestId('logout');
  }

  async getNomeUsuario() {
    // 'this.nomeUsuario' é o locator armazenado no constructor
    return await this.nomeUsuario.textContent();
  }

  async logout() {
    // 'this.botaoLogout' é o locator do botão
    await this.botaoLogout.click();
  }

  async verificarUsuario(nomeEsperado) {
    // Métodos podem chamar outros métodos via 'this'
    const nome = await this.getNomeUsuario();
    if (nome !== nomeEsperado) {
      throw new Error(`Esperava ${nomeEsperado}, recebeu ${nome}`);
    }
  }
}

Métodos

Métodos são funções definidas em uma classe. Podem ser síncronos ou async.

class SearchPage {
  constructor(page) {
    this.page = page;
    this.campoBusca = page.getByTestId('search-input');
    this.botaoBusca = page.getByTestId('search-button');
    this.resultados = page.getByTestId('search-result');
    this.mensagemSemResultados = page.getByTestId('no-results');
  }

  // Método async simples
  async buscar(query) {
    await this.campoBusca.fill(query);
    await this.botaoBusca.click();
  }

  // Método que retorna um valor
  async getQuantidadeResultados() {
    return await this.resultados.count();
  }

  // Método com múltiplos passos
  async buscarEVerificar(query, quantidadeEsperada) {
    await this.buscar(query);
    await this.page.waitForLoadState('networkidle');
    const quantidade = await this.getQuantidadeResultados();
    return quantidade === quantidadeEsperada;
  }
}

Herança com extends

Herança permite que uma classe construa sobre outra. No POM, você costuma ter uma BasePage que todas as páginas estendem.

// Classe base com comportamento comum
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 tirarScreenshot(nome) {
    await this.page.screenshot({ path: `screenshots/${nome}.png` });
  }
}

// Classe filha herda tudo da BasePage
class LoginPage extends BasePage {
  constructor(page) {
    super(page);  // Obrigatório como primeira linha — roda o constructor da BasePage
    this.emailInput = page.getByTestId('email-input');
    this.passwordInput = page.getByTestId('password-input');
    this.submitButton = page.getByTestId('submit-btn');
    this.mensagemErro = page.getByTestId('error-message');
  }

  async login(email, senha) {
    await this.navigate('/login');  // Herdado da BasePage
    await this.emailInput.fill(email);
    await this.passwordInput.fill(senha);
    await this.submitButton.click();
  }

  async getTextoErro() {
    return await this.mensagemErro.textContent();
  }
}

Agora LoginPage tem seus próprios métodos e tudo da BasePage:

const loginPage = new LoginPage(page);
await loginPage.login('usuario@teste.com', 'senha');  // método da LoginPage
await loginPage.tirarScreenshot('apos-login');         // método da BasePage
const titulo = await loginPage.getTitle();             // método da BasePage

super: chamando o pai

super() chama o constructor da classe pai. Obrigatório como primeira linha no constructor filho.

class AdminPage extends BasePage {
  constructor(page) {
    super(page);  // Roda o constructor da BasePage: define this.page, this.header, this.footer
    // Agora adicione as coisas específicas do AdminPage
    this.tabelaUsuarios = page.getByTestId('users-table');
    this.botaoAdicionarUsuario = page.getByTestId('add-user');
  }
}

Você também pode chamar métodos do pai com super.nomeDoMetodo():

class CheckoutPage extends BasePage {
  async navigate() {
    // Chama o navigate do pai, depois faz algo a mais
    await super.navigate('/checkout');
    await this.page.waitForSelector('[data-testid="checkout-form"]');
  }
}

Métodos estáticos

Métodos estáticos pertencem à classe, não às instâncias. Úteis para funções utilitárias.

class DadosDeTeste {
  static gerarEmail() {
    return `usuario_${Date.now()}@teste.com`;
  }

  static gerarUsuario() {
    return {
      email: DadosDeTeste.gerarEmail(),
      senha: 'SenhaValida1',
      nome: 'Usuário Teste',
    };
  }

  static gerarProduto(overrides = {}) {
    return {
      nome: 'Produto Teste',
      preco: 99.99,
      categoria: 'eletronicos',
      ...overrides,
    };
  }
}

// Use sem 'new' — chame diretamente na classe
const email = DadosDeTeste.gerarEmail();
const usuario = DadosDeTeste.gerarUsuario();
const produto = DadosDeTeste.gerarProduto({ preco: 49.99 });

Getters

Getters parecem propriedades, mas rodam uma função quando acessados:

class CartPage extends BasePage {
  constructor(page) {
    super(page);
    this.itens = page.getByTestId('cart-item');
    this.elementoTotal = page.getByTestId('cart-total');
  }

  get url() {
    return '/carrinho';  // Propriedade computada
  }

  async navigate() {
    await super.navigate(this.url);  // Usa o getter
  }

  async getQuantidadeItens() {
    return await this.itens.count();
  }
}

const cartPage = new CartPage(page);
console.log(cartPage.url);  // '/carrinho' — sem () necessário

Um exemplo completo de page object

Juntando tudo em um page object realista:

class ProductListPage extends BasePage {
  constructor(page) {
    super(page);
    this.cardsProduto = page.getByTestId('product-card');
    this.filtroCategoria = page.getByTestId('category-filter');
    this.dropdownOrdenacao = page.getByTestId('sort-select');
    this.campoBusca = page.getByTestId('product-search');
    this.spinnerCarregamento = page.getByTestId('loading');
  }

  async navigate() {
    await super.navigate('/products');
    await this.spinnerCarregamento.waitFor({ state: 'hidden' });
  }

  async filtrarPorCategoria(categoria) {
    await this.filtroCategoria.selectOption(categoria);
    await this.spinnerCarregamento.waitFor({ state: 'hidden' });
  }

  async ordenarPor(opcao) {
    await this.dropdownOrdenacao.selectOption(opcao);
  }

  async buscar(query) {
    await this.campoBusca.fill(query);
    await this.campoBusca.press('Enter');
    await this.spinnerCarregamento.waitFor({ state: 'hidden' });
  }

  async getQuantidadeProdutos() {
    return await this.cardsProduto.count();
  }

  async getNomesProdutos() {
    return await this.cardsProduto.getByTestId('product-name').allTextContents();
  }

  async clicarProduto(indice) {
    await this.cardsProduto.nth(indice).click();
  }
}

// Nos testes
test('filtrar produtos por categoria', async ({ page }) => {
  const produtos = new ProductListPage(page);
  await produtos.navigate();
  await produtos.filtrarPorCategoria('eletronicos');
  
  const quantidade = await produtos.getQuantidadeProdutos();
  expect(quantidade).toBeGreaterThan(0);
});

Classe vs objeto literal

Para coisas pontuais, um objeto literal é mais simples. Para algo que você reutiliza em vários testes, uma classe faz sentido.

// Objeto literal — bom para constantes
const USUARIOS_TESTE = {
  admin: { email: 'admin@teste.com', senha: 'AdminPass1' },
  membro: { email: 'membro@teste.com', senha: 'MembroPass1' },
};

// Classe — boa para page objects usados em muitos testes
class LoginPage extends BasePage {
  // ...
}

Resumo

  • constructor(page) — roda no new, armazene locators aqui
  • this — se refere à instância; como os métodos acessam os dados do objeto
  • extends — herdar de outra classe (ex.: BasePage)
  • super() — chama o constructor pai; obrigatório como primeira linha no constructor filho
  • static — métodos no nível da classe, sem precisar de instância
  • Métodos podem ser async: retornam promises, use await dentro deles

Classes nos Page Objects do Playwright não têm nada de mágico. São só a forma do JavaScript de agrupar locators e métodos relacionados em uma unidade reutilizável. Quando você entende constructor, this e extends, o resto vem naturalmente.

→ Veja também: Page Object Model no Playwright: Do Caos à Manutenibilidade | Interfaces e Tipos TypeScript para o Page Object Model | JavaScript para QA Engineers: O Mínimo para Começar a Automatizar