Playwright Storage State
Logging in through the UI before every test is the slowest, flakiest part of most suites. Playwright's storage state lets you log in once, save the browser's authentication data to a file, and start every test already logged in. This guide shows the recommended setup, multiple user roles, logging in through an API, and the pitfalls to avoid.
What Is Storage State?
After you log in, the browser keeps authentication data — usually a session cookie or a token in local storage. storageState() saves that data to a JSON file; a new browser context created with that file starts with the same data, so the app treats it as logged in.
| Details | |
|---|---|
| Saved with | context.storageState({ path }) or request.storageState({ path }) |
| Contains | Cookies and local storage per origin (IndexedDB optionally in recent versions) |
| Does not contain | Session storage — it's tied to a single tab and isn't saved |
| Reused with | storageState option on a project, a describe block or browser.newContext() |
Interview answer: "Storage state saves a browser context's cookies and local storage to a JSON file after login. A setup project logs in once and saves the file; the test projects load it, so every test starts authenticated without repeating the login — which makes suites faster and more stable."
Log In Once: The Setup Project Pattern
1. The setup file
// tests/auth/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(
process.env.TEST_USER ?? ''
);
await page.getByLabel('Password').fill(
process.env.TEST_PASSWORD ?? ''
);
await page.getByRole('button', { name: 'Log in' }).click();
await expect(
page.getByRole('heading', { name: 'Dashboard' })
).toBeVisible(); // wait until really logged in
await page.context().storageState({ path: authFile });
});
2. The config
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://app.example.com'
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup'], // runs the login first
},
],
});
Every test in the chromium project now starts logged in — no login code in the tests themselves.
3. Keep It Out of Git
# .gitignore
playwright/.auth/
The file contains live session cookies or tokens — treat it like a password.
Multiple Roles and Logged-Out Tests
Save one file per role in setup (admin.json, user.json), then choose per test group:
test.describe('admin area', () => {
test.use({
storageState: 'playwright/.auth/admin.json'
});
test('admin sees users page', async ({ page }) => {
await page.goto('/admin/users');
await expect(
page.getByRole('heading', { name: 'Users' })
).toBeVisible();
});
});
test.describe('logged out', () => {
test.use({
storageState: {
cookies: [],
origins: []
}
}); // start with an empty session
test('redirects to login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL(/login/);
});
});
To test two users interacting at once — for example, an admin approves something and a user sees the result — create two contexts in one test with browser.newContext({ storageState: … }).
Log In Through the API (Faster)
Skip the login page entirely: call the login endpoint with the request fixture and save its cookies:
setup('authenticate via API', async ({ request }) => {
const res = await request.post('/api/login', {
data: {
email: process.env.TEST_USER,
password: process.env.TEST_PASSWORD
},
});
expect(res.ok()).toBeTruthy();
await request.storageState({
path: 'playwright/.auth/user.json'
});
});
This works when the app authenticates with cookies. If the app keeps its token in local storage, log in through the UI once instead — or set the token with page.evaluate() — so local storage is captured too.
More: Playwright API Testing and API Authentication (OAuth, JWT).
What About CAPTCHA?
Automated tests shouldn't solve CAPTCHAs. Test environments usually disable them with a feature flag or test keys, or tests log in through the API — then storage state takes care of the rest. Production keeps the real CAPTCHA.
Cookies vs Local Storage vs Session Storage
| Cookies | Local storage | Session storage | |
|---|---|---|---|
| Size | Small (about 4 KB each) | Larger (a few MB per origin) | Larger (a few MB) |
| Sent to the server | Automatically, with every matching request | No — read by JavaScript | No |
| Lifetime | Until expiry, or the browser session | Until cleared | Until the tab closes |
| Common use | Session IDs, auth cookies | Tokens, preferences | Temporary per-tab state |
Saved by storageState() |
✅ | ✅ | ❌ |
Analogy: a cookie is a bus ticket the conductor checks on every ride (sent with every request); local storage is a notebook you keep in your bag (only you read it).
Pitfalls
- Expired sessions — if tokens expire quickly, regenerate the file in the setup project on every run (the default with
dependencies) rather than committing it. - Saving too early — wait for something that proves login finished, such as a dashboard heading, before calling
storageState(), or you save a half-logged-in state. - Tests that change the account — tests that log out, change passwords or delete the user can invalidate the shared session; give them their own account or context.
- Session storage apps — if your app stores auth in session storage, save and restore it yourself with
page.evaluate()or an init script. - Parallel workers and server state — one shared account across many workers can collide on server-side data such as carts and settings; use one account per worker when that matters.
Moving from Selenium, where the usual equivalent is adding cookies to the driver? Compare approaches in the Selenium ⇄ Playwright ⇄ Cypress Translator.
From Real Projects
I've built my automation skills on Selenium with Java and TestNG, and that background maps well onto Playwright: page classes become page objects, TestNG groups and parallel runs become projects and workers, and explicit waits become auto-waiting and web-first assertions. If you're coming from Selenium too, focus on those differences first. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. Logging in once and reusing the session is one of the biggest speed-ups for UI suites. Related: Playwright vs Selenium vs Cypress + Framework Architecture.
📚 Official documentation: Playwright documentation
FAQs
What Is Storage State in Playwright?
A JSON snapshot of a browser context's cookies and local storage, saved with storageState() and loaded into new contexts so tests start logged in.
Does Storage State Include Session Storage?
No. Session storage is tab-specific and isn't saved; handle it manually with page.evaluate() if your app relies on it.
How Do You Log In Once and Reuse the Session?
Log in in a setup project, save storageState to a file, and set that file as use.storageState in the test projects, with dependencies: ['setup'].
How Do You Test Multiple User Roles?
Save one storage file per role and use test.use({ storageState: '…' }) per describe block, or create separate contexts in one test.
How Do You Run a Test Logged Out?
Override with an empty state:
test.use({
storageState: {
cookies: [],
origins: []
}
});
Should the Auth File Be Committed?
No — it contains live credentials. Add playwright/.auth/ to .gitignore and regenerate it in CI.
Related
- The Complete Playwright Tutorial
- Test Runner, Parallel Runs & Retries
- Playwright Cheat Sheet
- Top 20 Playwright Interview Questions