Login once, run all tests already authenticated. Playwright saves browser state to a file and reuses it — no login flow repeated for every test.
The problem: login in every test
Login once in setup, share the auth file across all tests
If you have 80 tests that all need to be logged in — and you log in at the start of each one — that's 80 login flows per run. Each login is a network round-trip, a page load, a form fill. On a real app this adds up fast.
The solution: log in once, save the browser state (cookies + localStorage) to a JSON file, and tell every test to start from that file. Playwright calls this storageState.
Recommended setup: auth setup project
The cleanest approach: create a separate setup project that runs before your tests. It logs in and saves the state to playwright/.auth/user.json. Then every test project uses that file as its starting storageState.
First, create the directory and add it to .gitignore — you don't want auth files in your repo:
// tests/auth.setup.tsimport { test as setup, expect } from'@playwright/test'import path from'path'const authFile = path.join(__dirname, '../playwright/.auth/user.json')
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('admin@example.com')
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
await page.getByRole('button', { name: 'Sign in' }).click()
// Wait for redirect — cookies are set after thisawait page.waitForURL('/dashboard')
awaitexpect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
// Save cookies + localStorage to fileawait page.context().storageState({ path: authFile })
})
ts
// playwright.config.tsexportdefault defineConfig({
projects: [
// This project runs first and produces playwright/.auth/user.json
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json', // start every test authenticated
},
dependencies: ['setup'], // always run setup first
},
],
})
Multiple accounts for parallel tests
The shared auth file works great when tests only read data. If your tests modify server state (create orders, change settings), parallel tests will step on each other's changes when sharing one account. The fix: one account per parallel worker.
Each worker gets its own auth file named by its index (0.json, 1.json, ...). The worker logs in once, saves state, and all its tests reuse that state.
If your app has a login API endpoint, you can skip the browser form entirely and get a token or session cookie directly via an HTTP request. This is much faster than driving a login form through the UI.
ts
// tests/auth.setup.ts — API login approachimport { test as setup, request } from'@playwright/test'const authFile = 'playwright/.auth/user.json'
setup('authenticate via API', async ({ }) => {
const apiCtx = await request.newContext()
const response = await apiCtx.post('/api/auth/login', {
data: {
email: 'admin@example.com',
password: process.env.TEST_PASSWORD,
}
})
// Save cookies from the API response to the auth fileawait apiCtx.storageState({ path: authFile })
await apiCtx.dispose()
})
Multiple roles: admin and user
If your app has roles (admin, manager, viewer), create a separate auth file for each. Then in the test fixture, pick the right file based on what the test needs.