Quando o botão "Submit" é renomeado para "Sign In", uma suite sem Page Object Model exige atualizar cada teste que o clica. Com POM, você muda uma linha no LoginPage.ts e o restante acompanha. O padrão tem um uso errado comum: colocar assertions expect() dentro dos métodos do page object. Isso torna as mensagens de falha ambíguas e quebra a separação entre como interagir com a página e o que o teste verifica.
O que o Page Object Model resolve de verdade
POM é um padrão de design, não uma funcionalidade do Playwright. A ideia: criar uma classe que representa uma página (ou uma seção dela). A classe contém os locators e os métodos que interagem com essa página. Os testes usam a classe em vez de comandos brutos do Playwright.
Sem POM, seus testes ficam assim:
test('usuário consegue fazer login', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('My Travel Items')).toBeVisible();
});
test('login falha com senha errada', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('senhaerrada');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
});Com POM:
test('usuário consegue fazer login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'testpass123');
await expect(page.getByText('My Travel Items')).toBeVisible();
});
test('login falha com senha errada', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'senhaerrada');
await expect(page.getByText('Invalid credentials')).toBeVisible();
});Quando o botão muda de "Submit" para "Sign In", você corrige no LoginPage.ts. Os dois testes continuam verdes sem precisar tocá-los.
Construindo seu primeiro Page Object
Crie uma pasta pages/ na raiz do projeto. Dentro dela, LoginPage.ts:
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly loginButton: Locator;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.loginButton = page.getByRole('button', { name: 'Login' });
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Submit' });
}
async goto() {
await this.page.goto('https://lab.becomeqa.com');
}
async login(username: string, password: string) {
await this.loginButton.click();
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Dois pontos importantes. Primeiro, os locators são definidos no construtor como propriedades da classe. Essa é a abordagem recomendada pelo Playwright. Locators são lazy: não buscam o DOM quando definidos, apenas quando usados. Segundo, os métodos representam ações do usuário (goto, login), não cliques individuais. Os testes chamam ações, não detalhes de implementação.
Adicionando mais páginas
Adicione um DashboardPage.ts para a página após o login:
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly heading: Locator;
readonly itemsTable: Locator;
readonly addItemButton: Locator;
constructor(page: Page) {
this.page = page;
this.heading = page.getByRole('heading', { name: 'My Travel Items' });
this.itemsTable = page.getByRole('table');
this.addItemButton = page.getByRole('button', { name: 'Add Item' });
}
async isLoaded() {
await this.heading.waitFor({ state: 'visible' });
}
async getRowCount() {
const rows = this.page.getByRole('row');
return await rows.count() - 1; // subtrair a linha do cabeçalho
}
async clickAddItem() {
await this.addItemButton.click();
}
}Os testes agora leem como um fluxo de usuário:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('dashboard mostra itens após login', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'testpass123');
await dashboardPage.isLoaded();
const rowCount = await dashboardPage.getRowCount();
expect(rowCount).toBeGreaterThan(0);
});Use uma BasePage para comportamentos compartilhados
Se você tem 10 page objects e todos precisam da mesma lógica de navegação ou de um método waitForLoad comum, coloque isso em uma classe base:
// pages/BasePage.ts
import { Page } from '@playwright/test';
export class BasePage {
constructor(protected page: Page) {}
async waitForNetworkIdle() {
await this.page.waitForLoadState('networkidle');
}
async getPageTitle() {
return await this.page.title();
}
async scrollToBottom() {
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
}
}// pages/LoginPage.ts
import { Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
readonly loginButton: Locator;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: any) {
super(page);
this.loginButton = page.getByRole('button', { name: 'Login' });
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Submit' });
}
async goto() {
await this.page.goto('https://lab.becomeqa.com');
}
async login(username: string, password: string) {
await this.loginButton.click();
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Não exagere na herança. Um nível (BasePage para página específica) geralmente é suficiente. Cadeias de herança profundas ficam difíceis de acompanhar.
Transformando page objects em fixtures
Criar new LoginPage(page) em cada teste é repetitivo. As fixtures do Playwright permitem injetar page objects automaticamente:
// fixtures/pages.fixture.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type PageFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<PageFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';Agora importe test do seu arquivo de fixture em vez de importar do Playwright:
// tests/login.spec.ts
import { test, expect } from '../fixtures/pages.fixture';
test('usuário consegue fazer login', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'testpass123');
await expect(loginPage.page.getByText('My Travel Items')).toBeVisible();
});
test('dashboard mostra itens após login', async ({ loginPage, dashboardPage }) => {
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'testpass123');
await dashboardPage.isLoaded();
const rowCount = await dashboardPage.getRowCount();
expect(rowCount).toBeGreaterThan(0);
});Os page objects aparecem na assinatura do teste. Sem chamadas de construtor, sem imports de classes de página nos arquivos de teste. Só os objetos, prontos para usar.
O que pertence ao page object e o que não pertence
Coloque no page object:- Locators dos elementos da página
- Métodos que representam ações do usuário na página
- Condições de espera específicas da página (
isLoaded,waitForModal) - Getters simples de dados (
getRowCount,getHeadingText)
- Assertions (
expect(...)) - Dados de teste
- Lógica do teste (o que fazer e em qual ordem)
- Descrições do que o teste verifica
A regra: page objects descrevem COMO interagir com a página. Testes descrevem O QUE verificar. Colocar assertions dentro de page objects torna os testes mais difíceis de ler e as falhas mais difíceis de diagnosticar.
// Errado — assertion dentro do page object
async login(username: string, password: string) {
await this.loginButton.click();
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
await expect(this.page.getByText('My Travel Items')).toBeVisible(); // não faça isso
}
// Certo — page object apenas executa a ação
async login(username: string, password: string) {
await this.loginButton.click();
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
// O teste faz a assertion
await loginPage.login('admin@becomeqa.com', 'testpass123');
await expect(page.getByText('My Travel Items')).toBeVisible();Estrutura de projeto com POM
Uma estrutura de pastas limpa para um projeto baseado em POM:
project/
pages/
BasePage.ts
LoginPage.ts
DashboardPage.ts
ItemsPage.ts
fixtures/
pages.fixture.ts
auth.fixture.ts
tests/
auth/
login.spec.ts
logout.spec.ts
items/
items-list.spec.ts
items-crud.spec.ts
api/
items-api.spec.ts
playwright.config.ts
package.jsonFAQ
Toda página do app precisa ter um page object?Só as páginas que têm testes automatizados. Se você tem uma página de configurações sem testes, não crie um SettingsPage.ts só para ter.
Divida por seção. Uma página com tabela, modal e barra lateral pode virar ItemsTablePage, AddItemModal e um ItemsPage pai que os compõe. Ou extraia o modal para sua própria classe e importe-a na página.
O padrão às vezes é chamado de "API Objects": uma classe que encapsula chamadas de API relacionadas da mesma forma que um page object encapsula interações de UI. É útil quando muitos testes acessam os mesmos endpoints. Não é obrigatório, mas os mesmos benefícios se aplicam.
Quando devo migrar de sem-POM para POM?Quando você se pegar copiando e colando as mesmas 4 ou 5 linhas pela terceira vez. Esse é o sinal. Não construa page objects especulativamente antes de saber que vai precisar deles.
→ Veja também: Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Padrões Avançados de Page Object no Playwright | Interfaces e Tipos TypeScript para o Page Object Model | Classes JavaScript para Engenheiros QA: Construindo Page Objects