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:

  1. Speed – Skip UI login flows and jump straight to the page under test.
  2. Stability – Remove a common source of test flakiness (captchas, 2FA, throttling).
  3. 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.

Playwright Storage State Flow


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!

playwright testing automation storageState E2E

Share this post

Link copied!