Guia Pratico de Testes E2E com Playwright - Automacao de Testes de Navegador Modernos

Intermediario | 60 min leitura | 2025.12.02

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

  1. Configuracao do ambiente Playwright
  2. Como escrever testes basicos
  3. Implementacao do Page Object Model
  4. Testes de fluxo de autenticacao
  5. Mocks e interceptacao de API
  6. 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

  1. Localizadores: Priorize localizadores do ponto de vista do usuario (getByRole, getByLabel)
  2. Page Object: Adote POM para manutenibilidade em testes de larga escala
  3. Autenticacao: Reutilize estado de autenticacao com storageState
  4. Mock de API: Testes independentes do backend com route()
  5. Integracao CI: Testes automatizados com GitHub Actions

Proximos passos

Testes E2E melhoram significativamente a qualidade do desenvolvimento. Aproveite os recursos poderosos do Playwright para construir testes confiaveis.

← Voltar para a lista