Quando você escreve async ({ page }) => {...} em um teste do Playwright, está recebendo uma fixture: o Playwright criou uma página de navegador nova antes do teste rodar e vai fechá-la automaticamente depois. Fixtures customizadas funcionam da mesma forma, declaradas com test.extend() e recebidas pelo nome na assinatura do teste. A diferença é que você define o setup e o teardown, com await use(value) como linha divisória.

O que é uma fixture

Uma fixture é um valor (ou objeto) que o Playwright prepara antes do seu teste rodar e limpa depois. É injeção de dependência para testes.

Em vez de escrever isso:

test('usuário consegue fazer login', async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // código do teste
  
  await page.close();
  await context.close();
  await browser.close();
});

Você escreve isso:

test('usuário consegue fazer login', async ({ page }) => {
  // page está pronta para usar — setup e teardown são gerenciados
});

O Playwright cuida do ciclo de vida. Você recebe uma page limpa para cada teste, e ela é fechada automaticamente depois.

Fixtures nativas

O Playwright fornece estas fixtures prontas para usar:

| Fixture | Tipo | O que é |

|---------|------|---------|

| page | Page | Uma nova página de navegador (aba) para cada teste |

| browser | Browser | A instância do navegador (compartilhada entre testes no worker) |

| context | BrowserContext | Contexto do navegador, como uma janela anônima |

| browserName | string | O navegador atual: 'chromium', 'firefox', 'webkit' |

| request | APIRequestContext | Cliente HTTP para requisições de API |

page

A fixture mais usada. Cada teste recebe sua própria página isolada. Após o teste, ela fecha automaticamente.

test('página carrega corretamente', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await expect(page).toHaveTitle(/BecomeQA/);
});

context

Um contexto de navegador é como uma janela anônima: tem seus próprios cookies, storage e sessão. Se você precisa de múltiplas páginas em um teste, crie-as a partir do mesmo contexto:

test('duas páginas compartilham a mesma sessão', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();
  
  await page1.goto('/login');
  // Faz login na page1
  
  // page2 também vê a sessão (mesmo contexto = mesmos cookies)
  await page2.goto('/dashboard');
  await expect(page2.getByTestId('user-name')).toBeVisible();
});

browser

Normalmente você não precisa de browser diretamente. Use quando precisar criar contextos com configurações específicas:

test('viewport mobile', async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 390, height: 844 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
  });
  const page = await context.newPage();
  await page.goto('/');
  // Teste em página com tamanho mobile
  await context.close();
});

request

Faz requisições HTTP sem navegador. Usado para testes de API e para configurar dados de teste via API antes de testes de UI.

test('cria usuário via API', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: { email: 'novo@test.com', password: 'ValidPass1' },
  });
  expect(response.status()).toBe(201);
});

browserName

Use para pular testes condicionalmente em navegadores específicos:

test('download de arquivo', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'API de download diferente no Firefox');
  // ...
});

Fixtures customizadas

O poder real das fixtures está em criar as suas. Fixtures customizadas funcionam exatamente como as nativas: declaradas uma vez, usadas em qualquer lugar por desestruturação.

Fixture simples: uma página pré-navegada

// fixtures/index.ts
import { test as base, expect, Page } from '@playwright/test';

type MyFixtures = {
  loggedInPage: Page;
};

export const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    // SETUP
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'ValidPass1');
    await page.click('[data-testid="submit"]');
    await page.waitForURL('/dashboard');
    
    // Dá ao teste acesso à página
    await use(page);
    
    // TEARDOWN (roda após o teste)
    // Nada necessário aqui — a página fecha automaticamente
  },
});

export { expect };

// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';  // importe SEU test, não de @playwright/test

test('dashboard mostra mensagem de boas-vindas', async ({ loggedInPage }) => {
  // Já logado — loggedInPage É a página, após o login
  await expect(loggedInPage.getByTestId('welcome')).toBeVisible();
});

Fixture com page object

O padrão mais comum: uma fixture que fornece uma classe de page object inicializada.

// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type PageObjects = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
};

export const test = base.extend<PageObjects>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
});

// tests/login.spec.ts
import { test, expect } from '../fixtures';

test('login com sucesso', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('user@test.com', 'ValidPass1');
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

Sem precisar instanciar page objects em cada teste.

Fixture customizada com teardown

Quando sua fixture cria algo que precisa de cleanup:

type TestFixtures = {
  testUser: { id: number; email: string; token: string };
};

export const test = base.extend<TestFixtures>({
  testUser: async ({ request }, use) => {
    // SETUP: cria um usuário
    const response = await request.post('/api/users', {
      data: {
        email: `test_${Date.now()}@example.com`,
        password: 'ValidPass1',
        role: 'member',
      },
    });
    const user = await response.json();
    
    // Faz login para obter token
    const loginResp = await request.post('/api/auth/login', {
      data: { email: user.email, password: 'ValidPass1' },
    });
    const { token } = await loginResp.json();
    
    // Dá acesso ao teste
    await use({ id: user.id, email: user.email, token });
    
    // TEARDOWN: deleta o usuário
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});

test('usuário pode atualizar perfil', async ({ page, testUser }) => {
  // testUser tem id, email, token — novo e único por teste
  await page.goto(`/users/${testUser.id}`);
  // ...
  // Após o teste, o usuário é deletado automaticamente
});

Escopo de fixture

Por padrão, fixtures têm escopo 'test' e são recriadas para cada teste. Você pode definir o escopo como 'worker' para fixtures caras e seguras de compartilhar:

export const test = base.extend<{}, { sharedToken: string }>({
  sharedToken: [async ({ request }, use) => {
    // Roda uma vez por worker, não uma vez por teste
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
  }, { scope: 'worker' }],
});

Use escopo 'worker' para coisas que são:

  • Caras de recriar (seed de banco de dados, geração de arquivos)
  • Somente leitura (tokens de autenticação que você só lê, não modifica)
  • Seguras de compartilhar (sem estado que um teste possa corromper para outro)

Combinando fixtures

Fixtures customizadas podem depender de outras fixtures, incluindo outras customizadas:

export const test = base.extend<{
  loginPage: LoginPage;
  authenticatedPage: Page;
}>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  // Esta fixture USA loginPage
  authenticatedPage: async ({ page, loginPage }, use) => {
    await loginPage.goto();
    await loginPage.login('user@test.com', 'ValidPass1');
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Estrutura de projeto limpa

projeto/
├── fixtures/
│   └── index.ts        ← exporta seu test estendido + expect
├── pages/
│   ├── LoginPage.ts
│   └── DashboardPage.ts
└── tests/
    ├── login.spec.ts   ← importa de fixtures/index.ts
    └── dashboard.spec.ts

Todos os arquivos de teste importam de fixtures/index.ts, não de @playwright/test diretamente. Isso significa que cada teste automaticamente tem acesso a todas as fixtures customizadas.

Resumo

| | Nativas | Customizadas |

|-|---------|-------------|

| Onde definidas | Internals do Playwright | test.extend() no seu codebase |

| Onde usadas | Qualquer teste com { page }, { request } etc. | Qualquer teste usando seu test exportado |

| Exemplos | page, browser, request | loginPage, testUser, authToken |

| Ciclo de vida | Playwright gerencia | Você define setup + await use() + teardown |

Fixtures são a forma mais limpa de compartilhar lógica de setup entre testes. Quando você começa a usá-las para page objects e estados autenticados, os testes ficam muito mais curtos e focados no que realmente estão testando.

→ Veja também: Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Estrutura de Testes no Playwright: describe, beforeEach, afterEach e Hooks | Autenticação no Playwright com storageState (Sem Login em Cada Teste)