Playwright e uma ferramenta de automacao de testes de navegador de proxima geracao da Microsoft. Suporta Chromium, Firefox e WebKit, permitindo testes E2E (End-to-End) rapidos e confiaveis. Neste artigo, voce aprendera desde os fundamentos do Playwright ate padroes de testes praticos atraves de codigo real.
O que voce vai aprender neste artigo
- Configuracao do ambiente Playwright
- Como escrever testes basicos
- Implementacao do Page Object Model
- Testes de fluxo de autenticacao
- Mocks e interceptacao de API
- Integracao com CI/CD
Configuracao do ambiente
Instalacao
# Para novo projeto
npm init playwright@latest
# Adicionar a projeto existente
npm install -D @playwright/test
npx playwright install
Durante a inicializacao, as seguintes opcoes serao exibidas.
? Do you want to use TypeScript or JavaScript? › TypeScript
? Where to put your end-to-end tests? › tests
? Add a GitHub Actions workflow? › true
? Install Playwright browsers? › true
Estrutura do projeto
project/
├── playwright.config.ts # Arquivo de configuracao
├── tests/
│ ├── example.spec.ts # Arquivo de teste
│ └── fixtures/ # Fixtures customizadas
│ └── auth.ts
├── pages/ # Page Objects
│ ├── login.page.ts
│ └── dashboard.page.ts
├── test-results/ # Resultados dos testes (gerado automaticamente)
└── playwright-report/ # Relatorio HTML (gerado automaticamente)
Arquivo de configuracao
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Diretorio de testes
testDir: './tests',
// Execucao paralela
fullyParallel: true,
// Configurar retentativas em ambiente CI
retries: process.env.CI ? 2 : 0,
// Numero de workers
workers: process.env.CI ? 1 : undefined,
// Configuracao de reporters
reporter: [
['html', { open: 'never' }],
['list'],
],
// Configuracoes comuns
use: {
// URL base
baseURL: 'http://localhost:3000',
// Configuracao de screenshots
screenshot: 'only-on-failure',
// Configuracao de trace
trace: 'on-first-retry',
// Gravacao de video
video: 'on-first-retry',
},
// Configuracao de navegadores
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Dispositivos moveis
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
],
// Inicializacao do servidor de desenvolvimento
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Escrevendo testes basicos
Primeiro teste
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Pagina inicial', () => {
test('O titulo e exibido corretamente', async ({ page }) => {
await page.goto('/');
// Verificar titulo
await expect(page).toHaveTitle(/My App/);
});
test('A navegacao funciona', async ({ page }) => {
await page.goto('/');
// Clicar no link
await page.getByRole('link', { name: 'About' }).click();
// Verificar URL
await expect(page).toHaveURL('/about');
// Verificar conteudo
await expect(page.getByRole('heading', { level: 1 })).toHaveText('About Us');
});
});
Estrategia de localizadores
No Playwright, e importante usar localizadores robustos (metodos de identificacao de elementos).
// Recomendado: localizadores do ponto de vista do usuario
page.getByRole('button', { name: 'Enviar' }); // ARIA role
page.getByLabel('Endereco de email'); // Label
page.getByPlaceholder('example@email.com'); // Placeholder
page.getByText('Login realizado com sucesso'); // Texto
page.getByTestId('submit-button'); // data-testid
// Nao recomendado: localizadores que dependem de detalhes de implementacao
page.locator('#submit-btn'); // ID (muda facilmente)
page.locator('.btn-primary'); // Classe (quebra com mudancas de estilo)
page.locator('div > button:nth-child(2)'); // Dependente de estrutura
Assercoes
import { test, expect } from '@playwright/test';
test('Varias assercoes', async ({ page }) => {
await page.goto('/products');
// Visibilidade
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).not.toBeVisible();
// Conteudo de texto
await expect(page.getByRole('heading')).toHaveText('Lista de produtos');
await expect(page.getByRole('heading')).toContainText('produtos');
// Atributos
await expect(page.getByRole('link')).toHaveAttribute('href', '/cart');
// Contagem
await expect(page.getByTestId('product-card')).toHaveCount(10);
// Habilitado/Desabilitado
await expect(page.getByRole('button', { name: 'Comprar' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Comprar' })).toBeDisabled();
// Valor de formulario
await expect(page.getByLabel('Quantidade')).toHaveValue('1');
});
Testes de formulario
Entrada e envio
test.describe('Formulario de contato', () => {
test('O envio do formulario e bem-sucedido', async ({ page }) => {
await page.goto('/contact');
// Preencher formulario
await page.getByLabel('Nome').fill('Joao Silva');
await page.getByLabel('Endereco de email').fill('joao@example.com');
await page.getByLabel('Assunto').selectOption('inquiry');
await page.getByLabel('Mensagem').fill('Conteudo da mensagem aqui.');
// Checkbox
await page.getByLabel('Concordo com a politica de privacidade').check();
// Enviar
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar mensagem de sucesso
await expect(page.getByRole('alert')).toHaveText('Envio concluido');
});
test('Erros de validacao sao exibidos', async ({ page }) => {
await page.goto('/contact');
// Enviar vazio
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar mensagens de erro
await expect(page.getByText('Nome e obrigatorio')).toBeVisible();
await expect(page.getByText('Endereco de email e obrigatorio')).toBeVisible();
});
});
Page Object Model
Para testes em larga escala, use o padrao Page Object Model para melhorar a manutenibilidade.
Criando Page Objects
// pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Endereco de email');
this.passwordInput = page.getByLabel('Senha');
this.submitButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// pages/dashboard.page.ts
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
readonly userMenu: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome-message');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
this.userMenu = page.getByTestId('user-menu');
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
}
Uso nos testes
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
test.describe('Funcionalidade de login', () => {
test('Pode fazer login com credenciais corretas', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
// Verificar redirecionamento para dashboard
await expect(page).toHaveURL('/dashboard');
await expect(dashboardPage.welcomeMessage).toContainText('Bem-vindo');
});
test('Erro e exibido com credenciais incorretas', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrongpassword');
await expect(loginPage.errorMessage).toHaveText('Endereco de email ou senha incorretos');
await expect(page).toHaveURL('/login');
});
});
Gerenciamento de estado de autenticacao
Salvando e reutilizando estado de autenticacao
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('Autenticacao', async ({ page }) => {
// Processo de login
await page.goto('/login');
await page.getByLabel('Endereco de email').fill('user@example.com');
await page.getByLabel('Senha').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
// Confirmar sucesso do login
await expect(page).toHaveURL('/dashboard');
// Salvar estado de autenticacao
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
// Setup de autenticacao
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Testes autenticados
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Fixtures customizadas
// tests/fixtures/auth.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../../pages/login.page';
import { DashboardPage } from '../../pages/dashboard.page';
type AuthFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage;
};
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page }, use) => {
// Login automatico
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
export { expect } from '@playwright/test';
// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth';
test('Usuario autenticado pode acessar o dashboard', async ({ authenticatedPage }) => {
await expect(authenticatedPage.welcomeMessage).toBeVisible();
});
Mocks e interceptacao de API
Mock de respostas de API
test.describe('Lista de produtos', () => {
test('Busca e exibe dados da API', async ({ page }) => {
// Mock da resposta da API
await page.route('**/api/products', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Produto A', price: 1000 },
{ id: 2, name: 'Produto B', price: 2000 },
{ id: 3, name: 'Produto C', price: 3000 },
]),
});
});
await page.goto('/products');
// Verificar que dados mockados sao exibidos
await expect(page.getByTestId('product-card')).toHaveCount(3);
await expect(page.getByText('Produto A')).toBeVisible();
});
test('Mensagem de erro e exibida quando API falha', async ({ page }) => {
await page.route('**/api/products', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/products');
await expect(page.getByText('Falha ao buscar dados')).toBeVisible();
});
});
Interceptacao e verificacao de requisicoes
test('Dados corretos sao enviados para API no envio do formulario', async ({ page }) => {
let capturedRequest: any = null;
// Interceptar requisicao
await page.route('**/api/contact', async route => {
capturedRequest = route.request().postDataJSON();
await route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
await page.goto('/contact');
await page.getByLabel('Nome').fill('Joao Silva');
await page.getByLabel('Endereco de email').fill('joao@example.com');
await page.getByRole('button', { name: 'Enviar' }).click();
// Verificar dados enviados
expect(capturedRequest).toEqual({
name: 'Joao Silva',
email: 'joao@example.com',
});
});
Testes de regressao visual
Comparacao de screenshots
test('Screenshot da pagina inicial', async ({ page }) => {
await page.goto('/');
// Screenshot da pagina inteira
await expect(page).toHaveScreenshot('homepage.png');
});
test('Screenshot de componente', async ({ page }) => {
await page.goto('/components');
// Apenas elemento especifico
const card = page.getByTestId('feature-card');
await expect(card).toHaveScreenshot('feature-card.png');
});
test('Design responsivo', async ({ page }) => {
await page.goto('/');
// Desktop
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page).toHaveScreenshot('homepage-desktop.png');
// Tablet
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
Integracao CI/CD
Configuracao do GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: test-results/
retention-days: 30
Tecnicas de debug
Debug no modo UI
# Executar no modo UI
npx playwright test --ui
# Debug de teste especifico
npx playwright test --debug tests/login.spec.ts
Trace Viewer
// playwright.config.ts
use: {
trace: 'on-first-retry', // Trace apenas em falha
// trace: 'on', // Sempre fazer trace
}
# Abrir arquivo de trace
npx playwright show-trace test-results/example-spec-ts/trace.zip
Exibindo console.log
test('Saida de debug', async ({ page }) => {
// Capturar console.log do navegador
page.on('console', msg => console.log('Browser log:', msg.text()));
await page.goto('/');
// Pause do Playwright (debugger)
await page.pause();
});
Comandos de execucao de testes
# Executar todos os testes
npx playwright test
# Arquivo especifico
npx playwright test tests/login.spec.ts
# Navegador especifico
npx playwright test --project=chromium
# Modo com interface (exibir navegador)
npx playwright test --headed
# Controlar paralelismo
npx playwright test --workers=4
# Re-executar apenas testes que falharam
npx playwright test --last-failed
# Filtrar por tag
npx playwright test --grep @smoke
# Abrir relatorio HTML
npx playwright show-report
Resumo
Resumindo os pontos-chave dos testes E2E com Playwright.
Boas praticas
- Localizadores: Priorize localizadores do ponto de vista do usuario (getByRole, getByLabel)
- Page Object: Adote POM para manutenibilidade em testes de larga escala
- Autenticacao: Reutilize estado de autenticacao com storageState
- Mock de API: Testes independentes do backend com route()
- Integracao CI: Testes automatizados com GitHub Actions
Proximos passos
- Documentacao oficial do Playwright
- Uso em conjunto com Testing Library
- Aproveitar Component Testing
Testes E2E melhoram significativamente a qualidade do desenvolvimento. Aproveite os recursos poderosos do Playwright para construir testes confiaveis.