Padrões de E2E com Playwright
Aplique o Page Object Model, estratégias de localizadores robustas e padrões de asserção para testes E2E de fácil manutenção.
Busque em todas as páginas da documentação
Aplique o Page Object Model, estratégias de localizadores robustas e padrões de asserção para testes E2E de fácil manutenção.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
import { test, expect, Page } from "@playwright/test";
// Estratégias de localizadores (prefira consultas acessíveis)
page.getByRole("button", { name: /submit/i });
page.getByLabel("Email");
page.getByText("Welcome back");
page.getByPlaceholder("Search...");
page.getByTestId("checkout-form");
// Navegação e espera
await page.goto("/products");
await page.waitForURL("/products/**");
await page.getByRole("link", { name: /details/i }).click();
// Interação com formulário
await page.getByLabel("Email").fill("alice@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: /sign in/i }).click();
// Asserções (tentativa automática)
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading")).toHaveText("Dashboard");
await expect(page.getByRole("alert")).toBeVisible();
await expect(page.getByRole("button")).toBeEnabled();
// Capturas de tela
await page.screenshot({ path: "screenshots/dashboard.png" });
await expect(page).toHaveScreenshot("dashboard.png");Quando usar isso: Ao escrever testes E2E que precisam ser de fácil manutenção, legíveis e resilientes a pequenas alterações na UI.
// e2e/pages/checkout-page.ts
import { Page, Locator, expect } from "@playwright/test";
export class CheckoutPage {
readonly page: Page;
readonly emailInput: Locator;
readonly nameInput: Locator;
readonly addressInput: Locator;
readonly cityInput: Locator;
readonly zipInput: Locator;
readonly cardNumberInput: Locator;
readonly submitButton: Locator;
readonly orderConfirmation: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.nameInput = page.getByLabel("Full name");
this.addressInput = page.getByLabel("Address");
this.cityInput = page.getByLabel("City");
this.zipInput = page.getByLabel("ZIP code");
this.cardNumberInput = page.getByLabel("Card number");
this.submitButton = page.getByRole("button", { name: /place order/i });
this.orderConfirmation = page.getByRole("heading", { name: /order confirmed/i });
this.errorMessage = page.getByRole("alert");
}
async goto() {
await this.page.goto("/checkout");
}
async fillShippingInfo(info: {
email: string;
name: string;
address: string;
city: string;
zip: string;
}) {
await this.emailInput.fill(info.email);
await this.nameInput.fill(info.name);
await this.addressInput.fill(info.address);
await this.cityInput.fill(info.city);
await this.zipInput.fill(info.zip);
}
async fillPayment(cardNumber: string) {
await this.cardNumberInput.fill(cardNumber);
}
async submitOrder() {
await this.submitButton.click();
}
async expectOrderConfirmed() {
await expect(this.orderConfirmation).toBeVisible();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}// e2e/pages/product-page.ts
import { Page, Locator, expect } from "@playwright/test";
export class ProductPage {
readonly page: Page;
readonly addToCartButton: Locator;
readonly cartCount: Locator;
readonly quantityInput: Locator;
constructor(page: Page) {
this.page = page;
this.addToCartButton = page.getByRole("button", { name: /add to cart/i });
this.cartCount = page.getByTestId("cart-count");
this.quantityInput = page.getByLabel("Quantity");
}
async goto(productSlug: string) {
await this.page.goto(`/products/${productSlug}`);
}
async setQuantity(quantity: number) {
await this.quantityInput.fill(String(quantity));
}
async addToCart() {
await this.addToCartButton.click();
}
async expectCartCount(count: number) {
await expect(this.cartCount).toHaveText(String(count));
}
}// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
import { ProductPage } from "./pages/product-page";
import { CheckoutPage } from "./pages/checkout-page";
test.describe("Checkout Flow", () => {
test("complete purchase from product page to confirmation", async ({ page }) => {
// Add product to cart
const productPage = new ProductPage(page);
await productPage.goto("premium-widget");
await productPage.setQuantity(2);
await productPage.addToCart();
await productPage.expectCartCount(2);
// Navigate to checkout
await page.getByRole("link", { name: /checkout/i }).click();
await expect(page).toHaveURL("/checkout");
// Fill checkout form
const checkoutPage = new CheckoutPage(page);
await checkoutPage.fillShippingInfo({
email: "alice@example.com",
name: "Alice Johnson",
address: "123 Main St",
city: "Springfield",
zip: "62701",
});
await checkoutPage.fillPayment("4242424242424242");
await checkoutPage.submitOrder();
// Verify confirmation
await checkoutPage.expectOrderConfirmed();
await expect(page).toHaveURL(/\/orders\/[a-z0-9]+/);
});
test("shows validation errors for empty form", async ({ page }) => {
const checkoutPage = new CheckoutPage(page);
await checkoutPage.goto();
await checkoutPage.submitOrder();
await checkoutPage.expectError("Email is required");
});
test("shows error for invalid card", async ({ page }) => {
const checkoutPage = new CheckoutPage(page);
await checkoutPage.goto();
await checkoutPage.fillShippingInfo({
email: "alice@example.com",
name: "Alice Johnson",
address: "123 Main St",
city: "Springfield",
zip: "62701",
});
await checkoutPage.fillPayment("0000000000000000");
await checkoutPage.submitOrder();
await checkoutPage.expectError("Invalid card number");
});
});O que isso demonstra:
expect() tentam novamente automaticamente até passarem ou expirarem (padrão de 5 segundos) -- não há necessidade de esperas manuaisfill() limpa o input primeiro e depois digita o valor -- diferente de type() que anexa caractere por caractereclick() em um link espera a navegação completarComparação de estratégias de localizadores:
| Estratégia | Exemplo | Usar Quando |
|---|---|---|
getByRole | page.getByRole("button", { name: /submit/i }) | O elemento tem um papel ARIA (preferido) |
getByLabel | page.getByLabel("Email") | Inputs de formulário com rótulos |
getByText | page.getByText("Welcome") | Conteúdo de texto estático |
getByPlaceholder | page.getByPlaceholder("Search...") | Placeholders de input |
getByTestId | page.getByTestId("sidebar") | Nenhuma consulta semântica funciona |
locator | page.locator(".custom-dropdown") | Fallback de seletor CSS |
Testes de regressão visual:
test("product page visual", async ({ page }) => {
await page.goto("/products/widget");
// Comparação de captura de tela de página inteira
await expect(page).toHaveScreenshot("product-page.png", {
maxDiffPixelRatio: 0.01,
});
// Captura de tela de nível de elemento
const card = page.getByTestId("product-card");
await expect(card).toHaveScreenshot("product-card.png");
});
// A primeira execução cria capturas de tela base em __screenshots__/
// Execuções subsequentes comparam com as bases
// Atualizar bases: npx playwright test --update-snapshotsEstratégias de espera:
// Esperar pelo idle da rede (todas as requisições finalizadas)
await page.goto("/dashboard", { waitUntil: "networkidle" });
// Esperar por uma resposta específica
const response = await page.waitForResponse("/api/products");
expect(response.status()).toBe(200);
// Esperar pelo estado do elemento
await page.getByRole("button").waitFor({ state: "visible" });
await page.getByText("Loading").waitFor({ state: "hidden" });// Objetos de página se beneficiam de tipagem estrita
interface ShippingInfo {
email: string;
name: string;
address: string;
city: string;
zip: string;
}
// Estender fixtures de teste para objetos de página
import { test as base } from "@playwright/test";
type Fixtures = {
checkoutPage: CheckoutPage;
productPage: ProductPage;
};
export const test = base.extend<Fixtures>({
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
productPage: async ({ page }, use) => {
await use(new ProductPage(page));
},
});
// Usar em testes
test("checkout", async ({ checkoutPage, productPage }) => {
await productPage.goto("widget");
// ...
});Seletores CSS frágeis -- page.locator(".btn-primary.mt-4") quebra quando as classes mudam. Correção: Use getByRole, getByLabel ou getByTestId em vez disso.
Não esperar pela navegação -- Clicar em um link e fazer uma asserção imediatamente pode falhar se a página não carregou. Correção: O Playwright espera automaticamente em click, mas adicione await expect(page).toHaveURL(...) para verificação explícita.
Bases de captura de tela entre sistemas operacionais -- Fontes renderizam de forma diferente em macOS, Linux e Windows. Correção: Execute testes de captura de tela em Docker ou apenas em um SO na CI.
Objetos de página excessivamente granulares -- Criar um objeto de página para cada pequena seção adiciona indireção. Correção: Um objeto de página por página ou seção principal. Mantenha-o prático.
Usando page.waitForTimeout() -- Esperas codificadas tornam os testes lentos e instáveis. Correção: Use asserções de espera automática (expect(...).toBeVisible()) ou waitForResponse em vez disso.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Localizadores inline (sem POM) | Pequenas suítes de testes com poucas páginas | Testes crescem além de uma dúzia de arquivos |
| Objetos de página baseados em fixture | Você quer configuração automática de objeto de página via fixtures de teste | A construção simples de POM no corpo do teste é suficiente |
| Cypress | Você prefere a depuração time-travel do Cypress ou testes de componentes | Você precisa de suporte a múltiplos navegadores |
| Percy / Chromatic | Você precisa de regressão visual empresarial com fluxo de revisão | A comparação de captura de tela integrada é suficiente |
POM encapsula localizadores e interações para uma página em uma classe. Quando a UI muda, você atualiza o objeto de página uma vez em vez de cada teste que referencia essa página.
getByRole -- papel ARIA acessível (preferido)getByLabel -- rótulos de input de formuláriogetByText -- conteúdo de texto estáticogetByPlaceholder -- placeholders de inputgetByTestId -- último recursolocator() -- fallback de seletor CSSfill() limpa o input primeiro e depois define o valor.type() anexa caracteres um por um.fill() para a maioria dos testes de formulário.await expect(page).toHaveScreenshot("dashboard.png", {
maxDiffPixelRatio: 0.01,
});A primeira execução cria capturas de tela base. Execuções subsequentes comparam com elas. Atualize com --update-snapshots.
Fontes renderizam de forma diferente em macOS, Linux e Windows. Execute testes de captura de tela em Docker ou restrinja-os a um único SO na CI.
const response = await page.waitForResponse("/api/products");
expect(response.status()).toBe(200);export const test = base.extend<Fixtures>({
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
});
test("checkout", async ({ checkoutPage }) => { /* ... */ });Esperas codificadas tornam os testes lentos e instáveis. Use asserções de espera automática como expect(...).toBeVisible() ou waitForResponse em vez disso.
interface ShippingInfo {
email: string;
name: string;
address: string;
city: string;
zip: string;
}
async fillShippingInfo(info: ShippingInfo) { /* ... */ }Para suítes de testes pequenas com poucas páginas, localizadores inline são mais simples. POM adiciona indireção -- adote-o apenas quando os testes crescerem além de uma dúzia de arquivos ou as páginas forem reutilizadas em muitos testes.
Eles usam padrões de consulta acessíveis semelhantes (getByRole, getByLabel, getByText), mas os localizadores do Playwright são preguiçosos -- eles são avaliados quando uma ação ou asserção é realizada, não quando são criados.
const card = page.getByTestId("product-card");
await expect(card).toHaveScreenshot("product-card.png");Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥