Como Usar Testes End-to-End com Playwright e TypeScript: Page Objects Tipados em Produção Já leu

Entendendo Testes End-to-End e a Importância do Playwright Testes end-to-end (E2E) são aqueles que simulam o comportamento real do usuário no seu aplicativo, do início ao fim. Diferente dos testes unitários que isolam uma função ou dos testes de integração que testam módulos específicos, os E2E validam fluxos completos: login, navegação, preenchimento de formulários, e confirmação de resultados finais. O Playwright é uma ferramenta moderna que automatiza navegadores como Chrome, Firefox e Safari, permitindo escrever testes robustos e confiáveis. Por que escolher Playwright? Ele é mantido pela Microsoft, oferece suporte multiplataforma, é rápido, e possui excelente documentação. Quando combinado com TypeScript, você ganha tipagem estática — o compilador avisa erros antes da execução. Isso reduz bugs relacionados a propriedades inexistentes ou valores inválidos. Nesta aula, vamos além do básico e implementaremos o padrão Page Objects com tipagem completa, transformando seus testes em código profissional e mantível. O Padrão Page Object e Tipagem em TypeScript O que é Page Object? O

Entendendo Testes End-to-End e a Importância do Playwright

Testes end-to-end (E2E) são aqueles que simulam o comportamento real do usuário no seu aplicativo, do início ao fim. Diferente dos testes unitários que isolam uma função ou dos testes de integração que testam módulos específicos, os E2E validam fluxos completos: login, navegação, preenchimento de formulários, e confirmação de resultados finais. O Playwright é uma ferramenta moderna que automatiza navegadores como Chrome, Firefox e Safari, permitindo escrever testes robustos e confiáveis.

Por que escolher Playwright? Ele é mantido pela Microsoft, oferece suporte multiplataforma, é rápido, e possui excelente documentação. Quando combinado com TypeScript, você ganha tipagem estática — o compilador avisa erros antes da execução. Isso reduz bugs relacionados a propriedades inexistentes ou valores inválidos. Nesta aula, vamos além do básico e implementaremos o padrão Page Objects com tipagem completa, transformando seus testes em código profissional e mantível.

O Padrão Page Object e Tipagem em TypeScript

O que é Page Object?

O padrão Page Object encapsula os detalhes de uma página (ou componente) em uma classe. Em vez de espalhar seletores CSS ou XPath por toda a suite de testes, você centraliza a lógica de interação em objetos fortemente tipados. Um teste fica assim: await loginPage.fillEmail('user@example.com') em vez de await page.fill('input[name="email"]', 'user@example.com'). Isso torna os testes legíveis, mantíveis e reutilizáveis.

Tipagem Completa no TypeScript

TypeScript permite definir interfaces e classes com tipos explícitos. Quando criamos Page Objects tipados, definimos os retornos de cada método, validamos os parâmetros de entrada, e aproveitamos o intellisense da IDE. Se você tenta chamar um método que não existe ou passar um tipo inválido, o TypeScript grita antes do teste rodar. Essa segurança é especialmente valiosa em grandes suites de testes.

Estruturando seu Projeto Playwright + TypeScript

Instalação e Configuração Inicial

Comece criando um novo projeto Node.js com Playwright e TypeScript:

npm init -y
npm install --save-dev @playwright/test typescript @types/node
npx tsc --init

Configure o tsconfig.json com compilação moderna:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Crie a estrutura de diretórios:

projeto/
├── src/
│   ├── pages/
│   │   ├── basePage.ts
│   │   ├── loginPage.ts
│   │   └── dashboardPage.ts
│   ├── tests/
│   │   └── login.spec.ts
│   └── fixtures/
│       └── testFixtures.ts
├── playwright.config.ts
└── tsconfig.json

Configuração do Playwright

Crie playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './src/tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Implementando Page Objects Tipados

Base Page: A Fundação

Toda página herda de uma classe base que encapsula ações comuns. Isso evita duplicação de código e centraliza a lógica de espera e manipulação do Playwright:

import { Page, Locator } from '@playwright/test';

export class BasePage {
  protected page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async navigate(url: string): Promise<void> {
    await this.page.goto(url);
  }

  async fill(locator: Locator, value: string): Promise<void> {
    await locator.fill(value);
  }

  async click(locator: Locator): Promise<void> {
    await locator.click();
  }

  async getText(locator: Locator): Promise<string> {
    return await locator.textContent() || '';
  }

  async isVisible(locator: Locator): Promise<boolean> {
    return await locator.isVisible();
  }

  async waitForElement(locator: Locator, timeout: number = 5000): Promise<void> {
    await locator.waitFor({ state: 'visible', timeout });
  }
}

A classe BasePage é genérica e oferece métodos que qualquer página pode reutilizar. Note que todos os métodos são assíncronos (retornam Promise) — exigência do Playwright.

Login Page: Exemplo Prático

Agora criamos uma página específica para login. Defina interfaces para dados de entrada e saída, tornando o contrato explícito:

import { Page, Locator } from '@playwright/test';
import { BasePage } from './basePage';

interface LoginCredentials {
  email: string;
  password: string;
}

interface LoginResult {
  success: boolean;
  errorMessage?: string;
}

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorAlert: Locator;

  constructor(page: Page) {
    super(page);
    // Defina os seletores uma única vez na classe
    this.emailInput = page.locator('input[name="email"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.loginButton = page.locator('button[type="submit"]');
    this.errorAlert = page.locator('[role="alert"]');
  }

  async goToLoginPage(): Promise<void> {
    await this.navigate('/login');
  }

  async fillEmail(email: string): Promise<void> {
    await this.fill(this.emailInput, email);
  }

  async fillPassword(password: string): Promise<void> {
    await this.fill(this.passwordInput, password);
  }

  async clickLoginButton(): Promise<void> {
    await this.click(this.loginButton);
  }

  async login(credentials: LoginCredentials): Promise<void> {
    await this.goToLoginPage();
    await this.fillEmail(credentials.email);
    await this.fillPassword(credentials.password);
    await this.clickLoginButton();
  }

  async getErrorMessage(): Promise<string> {
    return await this.getText(this.errorAlert);
  }

  async isErrorDisplayed(): Promise<boolean> {
    return await this.isVisible(this.errorAlert);
  }

  async validateLoginResult(): Promise<LoginResult> {
    const isError = await this.isErrorDisplayed();
    if (isError) {
      return {
        success: false,
        errorMessage: await this.getErrorMessage(),
      };
    }
    return { success: true };
  }
}

Observe que:
- Seletores são propriedades da classe (emailInput, passwordInput) — você não repete eles em cada método.
- Métodos possuem nomes descritivosfillEmail() vs genérico fill().
- Interfaces tipam entradas e saídasLoginCredentials garante que você passa email e password.
- Métodos compostoslogin() agrupa ações lógicas que sempre andam juntas.

Dashboard Page: Outro Exemplo

Para demonstrar que o padrão escala, aqui está uma página do dashboard:

import { Page, Locator } from '@playwright/test';
import { BasePage } from './basePage';

interface UserInfo {
  name: string;
  email: string;
  role: string;
}

export class DashboardPage extends BasePage {
  readonly welcomeMessage: Locator;
  readonly userProfileButton: Locator;
  readonly logoutButton: Locator;
  readonly dataTable: Locator;

  constructor(page: Page) {
    super(page);
    this.welcomeMessage = page.locator('h1, h2');
    this.userProfileButton = page.locator('button[aria-label="User profile"]');
    this.logoutButton = page.locator('button:has-text("Logout")');
    this.dataTable = page.locator('table');
  }

  async getWelcomeText(): Promise<string> {
    return await this.getText(this.welcomeMessage);
  }

  async getUserInfo(): Promise<UserInfo> {
    // Exemplo fictício — adapte aos seletores reais
    const name = await this.page.locator('[data-testid="user-name"]').textContent() || '';
    const email = await this.page.locator('[data-testid="user-email"]').textContent() || '';
    const role = await this.page.locator('[data-testid="user-role"]').textContent() || '';

    return { name, email, role };
  }

  async logout(): Promise<void> {
    await this.click(this.userProfileButton);
    await this.click(this.logoutButton);
  }

  async isTableVisible(): Promise<boolean> {
    return await this.isVisible(this.dataTable);
  }
}

Escrevendo Testes com Page Objects Tipados

Teste de Login Bem-Sucedido

Agora que temos os Page Objects, escrever testes é simples e legível:

import { test, expect, Page } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';
import { DashboardPage } from '../pages/dashboardPage';

test.describe('Login Flow', () => {
  let loginPage: LoginPage;
  let dashboardPage: DashboardPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
  });

  test('should login successfully with valid credentials', async ({ page }) => {
    // Arrange
    const validCredentials = {
      email: 'user@example.com',
      password: 'SecurePassword123!',
    };

    // Act
    await loginPage.login(validCredentials);

    // Assert
    await expect(page).toHaveURL(/.*dashboard/);
    const welcomeText = await dashboardPage.getWelcomeText();
    expect(welcomeText).toContain('Welcome');
  });

  test('should display error with invalid credentials', async () => {
    // Arrange
    const invalidCredentials = {
      email: 'wrong@example.com',
      password: 'WrongPassword123!',
    };

    // Act
    await loginPage.login(invalidCredentials);

    // Assert
    const result = await loginPage.validateLoginResult();
    expect(result.success).toBe(false);
    expect(result.errorMessage).toContain('Invalid credentials');
  });

  test('should logout successfully', async ({ page }) => {
    // Arrange — fazer login primeiro
    const credentials = {
      email: 'user@example.com',
      password: 'SecurePassword123!',
    };

    // Act
    await loginPage.login(credentials);
    await page.waitForURL(/.*dashboard/);
    await dashboardPage.logout();

    // Assert
    await expect(page).toHaveURL(/.*login/);
  });
});

Veja como o teste é declarativo e fácil de ler. Quem não conhece Playwright ainda consegue entender o que está sendo testado apenas lendo o código.

Fixtures Customizados para Reutilização

Para evitar repetir a inicialização de Page Objects, crie um fixture customizado:

import { test as baseTest, Page } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';
import { DashboardPage } from '../pages/dashboardPage';

interface TestFixtures {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: Page;
}

export const test = baseTest.extend<TestFixtures>({
  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, loginPage }, use) => {
    await loginPage.login({
      email: 'user@example.com',
      password: 'SecurePassword123!',
    });
    await page.waitForURL(/.*dashboard/);
    await use(page);
  },
});

export { expect };

Agora seus testes ficam ainda mais limpios:

import { test, expect } from '../fixtures/testFixtures';

test('should display user info on dashboard', async ({ dashboardPage, authenticatedPage }) => {
  const userInfo = await dashboardPage.getUserInfo();
  expect(userInfo.name).toBeTruthy();
  expect(userInfo.email).toContain('@');
});

O fixture authenticatedPage já faz o login automaticamente — você não precisa repetir essa lógica em cada teste.

Boas Práticas e Padrões Avançados

Evitando Esperas Implícitas

O Playwright já espera por elementos automaticamente antes de interagir. Porém, às vezes você precisa de controle fino. Use waitForElement() com prudência:

async waitForLoadingToComplete(): Promise<void> {
  const spinner = this.page.locator('[data-testid="loading-spinner"]');
  await spinner.waitFor({ state: 'hidden', timeout: 10000 });
}

Encapsulando Validações Complexas

Não coloque assertions (expect) dentro dos Page Objects — eles devem apenas retornar dados. As assertions ficam nos testes:

// ❌ Evite isto no Page Object
async verifyUserName(expectedName: string): Promise<void> {
  const actualName = await this.getText(this.userNameElement);
  expect(actualName).toBe(expectedName); // Não faça isso aqui
}

// ✅ Faça isto
async getUserName(): Promise<string> {
  return await this.getText(this.userNameElement);
}

// ✅ E coloque a assertion no teste
test('should display correct user name', async ({ dashboardPage }) => {
  const name = await dashboardPage.getUserName();
  expect(name).toBe('John Doe');
});

Tratamento de Cenários de Erro

Crie métodos que retornam estados conhecidos:

async attemptLogin(credentials: LoginCredentials): Promise<LoginResult> {
  await this.login(credentials);
  try {
    await this.page.waitForURL(/.*dashboard/, { timeout: 3000 });
    return { success: true };
  } catch {
    const error = await this.getErrorMessage();
    return { success: false, errorMessage: error };
  }
}

Executando e Monitorando os Testes

Rodando os Testes

# Todos os testes
npx playwright test

# Apenas um arquivo
npx playwright test src/tests/login.spec.ts

# Modo watch (para desenvolvimento)
npx playwright test --watch

# Com UI interativo
npx playwright test --ui

# Modo debug (passo a passo)
npx playwright test --debug

Gerando Relatórios

O Playwright gera automaticamente relatórios HTML. Abra com:

npx playwright show-report

Conclusão

Ao dominar testes E2E com Playwright, TypeScript e Page Objects tipados, você constrói uma suite robusta, mantível e escalável. Os três pontos principais que levam você de iniciante a profissional são: (1) Page Objects centralizam seletores e lógica de página, eliminando duplicação e tornando mudanças de UI simples; (2) TypeScript com interfaces explícitas evita erros de tipagem em tempo de compilação, não de execução; (3) Fixtures customizados eliminam repetição de setup, deixando testes focados apenas na lógica de negócio.

Esses padrões não são apenas boas práticas — são investimentos que multiplicam sua produtividade conforme a suite cresce de 10 para 100 para 1000 testes.

Referências


Artigos relacionados