Mastering Playwright’s storageState()
– Persist Login Sessions for Faster, Flakeless Tests
TL;DR – Stop logging in on every test run. In this guide you’ll learn how to capture a logged-in browser once with Playwright’s
storageState()
and reuse that session across the entire test suite.
Introduction
Modern end-to-end (E2E) tests need to be fast and reliable. Re-entering credentials for every single test wastes precious minutes and increases flakiness whenever the login form changes. Luckily, Microsoft Playwright ships with a built-in way to persist authenticated state between test runs: storageState()
.
In this article we’ll walk through a real-world Playwright project that automatically signs in to LinkedIn, stores the browser’s cookies & localStorage, and then replays that state in all subsequent tests. You’ll learn:
- What
storageState()
does under the hood - How a global setup script captures the state once
- How to configure
playwright.config.ts
to inject the saved session - How to structure tests that assume you’re already logged in
By the end, you’ll be able to shave seconds off every CI run and eliminate flaky login steps for good.
What is storageState()
and why should you care?
storageState()
is a Playwright API that serializes the cookies, localStorage, and sessionStorage of the current browser context into a plain JSON file. You can later load this file to restore the exact same authenticated state—no repetitive UI logins required.
Benefits at a glance:
- Speed – Skip UI login flows and jump straight to the page under test.
- Stability – Remove a common source of test flakiness (captchas, 2FA, throttling).
- Simplicity – Write tests as if the user is already authenticated.
Project structure overview
pw-example-code/
├── playwright.config.ts
├── scripts/
│ └── setup/
│ └── login.setup.ts # global setup that logs in once and saves storageState
└── scripts/
└── src/
└── checkLogin.spec.ts # tests that reuse the saved session
Step-by-step implementation
1. Configure your environment variables
Create a .env
file at the project root:
LINKEDIN_EMAIL="your-email@example.com"
LINKEDIN_PASSWORD="superSecretPass123"
Tip: never commit real credentials; use CI secrets for automated pipelines.
2. Capture the session in a global setup script
The script below launches Chromium, performs a single login, and calls storageState({ path: 'playwright/.auth/user.json' })
.
// scripts/setup/login.setup.ts
import { chromium, Browser, Page } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
const email = process.env.LINKEDIN_EMAIL;
const password = process.env.LINKEDIN_PASSWORD;
export default async function globalSetup() {
const browser: Browser = await chromium.launch();
const page: Page = await browser.newPage();
await page.goto('https://www.linkedin.com/login');
if (!email || !password) {
throw new Error('LINKEDIN_EMAIL and LINKEDIN_PASSWORD must be set in .env');
}
await page.fill('input[name="session_key"]', email);
await page.fill('input[name="session_password"]', password);
await page.click('button[type="submit"]');
await page.waitForURL('https://www.linkedin.com/feed/');
// 👉 Save cookies + storage to JSON
await page.context().storageState({ path: 'playwright/.auth/user.json' });
await browser.close();
}
3. Wire everything up in playwright.config.ts
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
reporter: 'html',
globalSetup: require.resolve('./scripts/setup/login.setup.ts'),
use: {
storageState: 'playwright/.auth/user.json', // ← load the saved session
trace: 'on',
headless: false,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], headless: false },
},
],
});
4. Write a test that assumes you’re already logged in
// scripts/src/checkLogin.spec.ts
import { test, expect } from '@playwright/test';
test('user should land on LinkedIn feed (already authenticated)', async ({ page }) => {
await page.goto('https://www.linkedin.com/feed/');
await expect(page).toHaveURL(/\/feed/);
await page.getByRole('button', { name: 'Start a post' }).waitFor({ state: 'visible' });
});
Under the hood – Mermaid workflow diagram
Below is a high-level overview of the process with decision branches for missing credentials or storage state.
Key takeaways
- One-time login in global setup scripts is simpler and faster than logging in every test.
storageState()
captures cookies + web storage in a portable JSON file.- Point the
use.storageState
configuration to that file to hydrate new browser contexts. - Keep credentials safe by loading them from environment variables or CI secrets.
Conclusion
Persisting authentication with Playwright’s storageState()
is a powerful yet under-utilised feature. By capturing the session once, you remove redundancy, speed up your suite, and eliminate a major source of test instability. Whether you’re testing LinkedIn, an internal dashboard, or any other login-gated web app, this approach scales cleanly from local dev to CI pipelines.
Give it a try in your own project and watch your test runtime—and frustration—drop dramatically. Happy testing!
Share this post
Found this useful? Let others know!