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 BasePagesuper: 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árioUm 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 nonew, armazene locators aquithis— se refere à instância; como os métodos acessam os dados do objetoextends— herdar de outra classe (ex.:BasePage)super()— chama o constructor pai; obrigatório como primeira linha no constructor filhostatic— métodos no nível da classe, sem precisar de instância- Métodos podem ser async: retornam promises, use
awaitdentro 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.