storageState captura cookies e localStorage após um login real e os salva em um arquivo JSON. Cada contexto de teste subsequente carrega esse arquivo em vez de passar pelo login na UI. Isso corta 2 a 4 segundos por teste em suites onde a maioria dos testes exige autenticação.
O que storageState realmente salva
Quando você se autentica em um navegador, o servidor prova sua identidade por um de dois mecanismos. O mais comum é um cookie, geralmente um session ID ou JWT em um cookie HTTP-only. O outro é um token armazenado no localStorage ou sessionStorage. Às vezes os dois.
O storageState do Playwright captura tudo isso. Chamar context.storageState() retorna um objeto JSON com todos os cookies do contexto e um snapshot de localStorage e sessionStorage para cada origem. Você pode escrever esse JSON no disco, e quando o Playwright cria um novo contexto de navegador com storageState: './auth.json', ele pré-carrega todos esses dados antes da primeira navegação. Para o servidor, a requisição parece idêntica a uma vinda da sessão autenticada original.
// Como o arquivo salvo se parece (abreviado)
{
"cookies": [
{
"name": "session",
"value": "eyJhbGciOi...",
"domain": "lab.becomeqa.com",
"path": "/",
"expires": 1748000000,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://lab.becomeqa.com",
"localStorage": [
{ "name": "auth_token", "value": "eyJhbGciOi..." }
]
}
]
}O arquivo é apenas JSON. Você pode inspecioná-lo, commitá-lo em uma branch só de testes, ou regenerá-lo sob demanda. A maioria dos times o adiciona ao .gitignore e o regenera no início de cada execução de CI.
Configurando global-setup.ts
O padrão padrão é um arquivo global-setup.ts que roda uma vez antes de toda a suite de testes. Ele lança um navegador, realiza o login real pela UI e salva o estado resultante em um arquivo. Cada worker de teste então lê esse arquivo em vez de fazer login.
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// Aguarda até estar realmente no dashboard autenticado
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
// Salva cookies + localStorage em um arquivo
await context.storageState({ path: 'playwright/.auth/admin.json' });
await browser.close();
}
export default globalSetup;Crie o diretório antes de rodar os testes, ou o Playwright vai lançar um erro de arquivo não encontrado:
mkdir -p playwright/.authAdicione o diretório ao .gitignore para que tokens de auth não acabem no controle de versão:
# .gitignore
playwright/.auth/Conectando ao playwright.config.ts
Duas coisas precisam acontecer na sua config. Primeiro, diga ao Playwright onde global-setup.ts está. Segundo, diga a cada projeto de teste para usar o estado salvo como contexto inicial.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
use: {
baseURL: 'https://lab.becomeqa.com',
storageState: 'playwright/.auth/admin.json',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
});Isso é suficiente para eliminar o login de cada teste. O storageState definido em use se aplica globalmente, então todo contexto de navegador que o Playwright criar vai começar pré-autenticado.
const ADMIN_AUTH = 'playwright/.auth/admin.json'.O padrão de projeto setup (recomendado para suites maiores)
O hook globalSetup funciona, mas tem uma desvantagem: roda fora do sistema de projetos e reporter do Playwright. Falhas em global-setup.ts produzem output mínimo, e o setup não aparece no seu relatório HTML.
A alternativa recomendada, introduzida no Playwright 1.31, é um projeto setup dedicado. Ele roda antes dos outros projetos e se beneficia de todo o pipeline de reporting.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
// Projeto setup roda primeiro, produz arquivos de auth
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Projetos de teste dependem do setup completar primeiro
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
},
],
});O arquivo de setup em si é um arquivo de teste Playwright comum:
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('autenticar como admin', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});Agora o step de login aparece no seu relatório HTML. A lógica de retry se aplica se a página de login for flaky, e screenshots em caso de falha são capturadas automaticamente.
storageState por projeto para múltiplos roles de usuário
A maioria das aplicações tem mais de um role de usuário, e você precisa testá-los independentemente. Um usuário admin vê controles de gerenciamento. Um usuário regular não vê. Se você roda testes de admin com a sessão de um usuário regular, eles vão falhar pelo motivo errado.
Adicione um step de setup por role, um arquivo de auth por role, e um projeto de teste por role:
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('autenticar como admin', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});
setup('autenticar como usuário regular', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('user@becomeqa.com');
await page.getByLabel('Password').fill('userpass456');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const ADMIN_AUTH = 'playwright/.auth/admin.json';
const USER_AUTH = 'playwright/.auth/user.json';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Testes de admin
{
name: 'admin-chromium',
testMatch: /.*admin.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: ADMIN_AUTH,
},
dependencies: ['setup'],
},
// Testes de usuário regular
{
name: 'user-chromium',
testMatch: /.*user.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: USER_AUTH,
},
dependencies: ['setup'],
},
// Testes que não precisam de auth (landing page, testes do fluxo de login)
{
name: 'public',
testMatch: /.*public.*\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
},
],
});Cada projeto carrega um arquivo de auth diferente, e seus testes de admin nunca rodam acidentalmente com a sessão de um usuário regular.
public e usar o fixture page sem nenhum storageState. O ponto desses testes é passar pelo login na UI.Fixtures com escopo de worker para storageState (padrão avançado)
storageState definido no playwright.config.ts tem um problema sutil: ele se aplica ao contexto de navegador. Se um teste modifica o estado de autenticação, o contexto pode vazar para o próximo teste no mesmo worker. Isso acontece em ações como atualizar o perfil do usuário, mudar o email ou fazer logout.
A solução é criar um contexto novo por teste, carregado do arquivo de auth estático, em vez de compartilhar um contexto entre testes. Uma fixture com escopo de worker resolve isso de forma limpa:
// fixtures/auth.fixture.ts
import { test as base, BrowserContext } from '@playwright/test';
import path from 'path';
const ADMIN_AUTH = path.resolve('playwright/.auth/admin.json');
type AuthFixtures = {
// Escopo de worker: o caminho do arquivo de auth, carregado uma vez por worker
adminStorageState: string;
};
type TestFixtures = {
// Escopo de teste: um contexto novo por teste, carregado do arquivo de estado
adminContext: BrowserContext;
};
export const test = base.extend<TestFixtures, AuthFixtures>({
// Fixture de worker apenas guarda o caminho, valida que o arquivo existe uma vez
adminStorageState: [
async ({}, use) => {
await use(ADMIN_AUTH);
},
{ scope: 'worker' },
],
// Fixture de teste cria um contexto novo a partir do estado salvo
adminContext: async ({ browser, adminStorageState }, use) => {
const context = await browser.newContext({
storageState: adminStorageState,
});
await use(context);
await context.close();
},
});
export { expect } from '@playwright/test';Testes que usam essa fixture recebem um contexto de navegador isolado que começa autenticado, mas qualquer mudança de estado dentro do teste não afeta outros testes:
// tests/admin-items.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
test('admin pode ver o painel de gerenciamento', async ({ adminContext }) => {
const page = await adminContext.newPage();
await page.goto('/');
await expect(page.getByRole('link', { name: 'Admin Panel' })).toBeVisible();
});
test('admin pode deletar qualquer item', async ({ adminContext }) => {
const page = await adminContext.newPage();
await page.goto('/items');
await page.getByTestId('item-row-1').getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('item-row-1')).not.toBeVisible();
});Cada teste recebe seu próprio BrowserContext inicializado do arquivo de auth. A deleção no segundo teste não afeta nenhum estado compartilhado.
Combinando storageState com login via API (setup mais rápido)
O arquivo auth.setup.ts mostrado anteriormente faz um login completo pela UI: navega, clica, preenche formulários e aguarda. Funciona, mas ainda são vários segundos. Em uma máquina de CI lenta ou quando o formulário de login tem animações, pode ser o gargalo.
Se sua aplicação tem um endpoint de login na API, você pode chamá-lo diretamente do step de setup. Pule a UI por completo e escreva o token resultante no storage state manualmente. Isso é tipicamente 5 a 10 vezes mais rápido que a abordagem pela UI:
// tests/auth.setup.ts (versão via API)
import { test as setup, request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const AUTH_FILE = 'playwright/.auth/admin.json';
setup('autenticar como admin via API', async ({ request }) => {
// Chama o endpoint de login diretamente
const response = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
email: 'admin@becomeqa.com',
password: 'testpass123',
},
});
const { token, sessionCookie } = await response.json();
// Constrói a estrutura de storageState manualmente
const storageState = {
cookies: [
{
name: 'session',
value: sessionCookie,
domain: 'lab.becomeqa.com',
path: '/',
expires: Math.floor(Date.now() / 1000) + 86400, // 24 horas
httpOnly: true,
secure: true,
sameSite: 'Lax' as const,
},
],
origins: [
{
origin: 'https://lab.becomeqa.com',
localStorage: [
{ name: 'auth_token', value: token },
],
},
],
};
// Garante que o diretório existe
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
fs.writeFileSync(AUTH_FILE, JSON.stringify(storageState, null, 2));
});O trade-off: essa abordagem exige que você conheça a estrutura exata do armazenamento de auth da sua aplicação (quais cookies ela define, quais chaves de localStorage ela lê). A abordagem pela UI funciona independente dos detalhes de implementação. Você simplesmente faz login e salva o que o navegador acabou com. Comece pela abordagem de UI, mude para via API se o login se tornar um gargalo mensurável.
Quando storageState quebra
storageState não é mágico. É um snapshot do estado do navegador de um momento específico no tempo. Algumas coisas vão fazer com que pare de funcionar:
Expiração do token. Se sua aplicação usa JWTs de curta duração (15 minutos, 1 hora), o token salvo vai estar expirado quando os testes posteriores rodarem. A solução é regenerar o arquivo de auth no início de cada execução de CI (o que você deveria estar fazendo de qualquer forma). Ou mude para login via API, que sempre emite um token novo.
Invalidação de sessão pelo servidor. Algumas aplicações invalidam sessões quando detectam padrões anômalos. Múltiplas requisições simultâneas da "mesma" sessão entre diferentes processos worker é um desses padrões. Se você vê erros 401 aleatórios em testes que deveriam estar autenticados, verifique sua aplicação. Pode ser que proteções de fixação de sessão estejam tratando workers de teste paralelos como suspeitos.
Autenticação de dois fatores. 2FA quebra o setup de storageState via UI por completo. O fluxo de login requer um código TOTP ou verificação por SMS que você não pode automatizar pelo Playwright de forma geral. As soluções práticas são três. Usar uma conta de teste dedicada com 2FA desabilitado, se sua aplicação permite. Usar login via API que emite tokens sem 2FA para ambientes de teste. Ou adicionar uma variável de ambiente que contorna o 2FA quando NODE_ENV=test.
Sessões vinculadas ao navegador. Algumas aplicações vinculam sessões a fingerprints de navegador, certificados TLS de cliente, ou IDs de dispositivo. Se seus cookies de sessão têm atributos que os restringem a características específicas de dispositivo, salvar e restaurar entre diferentes instâncias de navegador não vai funcionar. Isso é incomum em aplicações web, mas vale saber.
// Verificando que o estado salvo ainda é válido. Adicione esta verificação ao auth.setup.ts.
setup('autenticar como admin', async ({ page }) => {
const AUTH_FILE = 'playwright/.auth/admin.json';
if (fs.existsSync(AUTH_FILE)) {
// Verifica se o token existente ainda é válido
const checkContext = await browser.newContext({ storageState: AUTH_FILE });
const checkPage = await checkContext.newPage();
await checkPage.goto('/');
const isAuthenticated = await checkPage.getByText('My Travel Items').isVisible();
await checkContext.close();
if (isAuthenticated) {
console.log('Estado de auth existente é válido, pulando o login');
return; // Reutiliza o arquivo existente
}
}
// Estado inválido ou ausente, faz o login completo
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: AUTH_FILE });
});playwright/.auth/*.json contém tokens de sessão reais que concedem acesso às suas contas de teste. Adicione o diretório ao .gitignore e rotacione as senhas das contas de teste regularmente. Se você usa variáveis de ambiente para credenciais (o que deve fazer no CI), certifique-se de que essas variáveis não sejam logadas no output do seu pipeline.storageState é a mudança com maior ROI que você pode fazer em uma suite Playwright lenta. O setup leva cerca de 30 minutos de trabalho e pode cortar o tempo total de teste em 20 a 30% em suites onde a maioria dos testes exige autenticação.
→ Veja também: Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Arquivo de Configuração do Playwright Explicado: Todas as Opções | Testes de API com o APIRequestContext do Playwright (Sem Postman) | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado | Configuração e Limpeza Global no Playwright