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.tsTodos 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)