|

Cookies and localStorage Manipulation in Playwright

Almost every flaky, slow end-to-end suite has the same root cause: tests log in through the UI over and over, and every test silently depends on whatever cookies and localStorage the previous test happened to leave behind. If you have ever watched a green suite turn red because a session token expired or a feature flag in storage went stale, you already know the pain. In this guide you will learn practical Playwright cookies localStorage manipulation in TypeScript: how to read, set, and clear cookies, how to seed and inspect localStorage and sessionStorage, and how to capture authenticated state once and reuse it so your tests start logged in and stay deterministic.

🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

Contents

Why manipulate cookies and localStorage in tests?

Browser storage is where web apps keep the state that survives a page reload: authentication tokens, user preferences, A/B test buckets, shopping carts, and cookie-consent decisions. When you control that storage directly from your tests, three big things change for the better.

  • Speed — you skip the login UI entirely by injecting a valid session, turning a 4-second flow into a few milliseconds.
  • Determinism — every test starts from a known storage state instead of inheriting leftovers from earlier tests.
  • Coverage — you can simulate hard-to-reach states (expired tokens, beta flags, dismissed banners) that would be tedious to produce through the UI.

Playwright exposes all of this through a clean API surface: cookies live on the BrowserContext, while localStorage and sessionStorage live in the page and are reached with page.evaluate(). Let’s start with cookies.

Working with cookies in Playwright

Cookies belong to the browser context, not to an individual page. That is intentional: a context is an isolated browser session, so any cookie you add is shared by every page and tab inside it. The two core methods are context.addCookies() and context.cookies(), with context.clearCookies() to wipe them.

import { test, expect } from '@playwright/test';

test('set and read a cookie', async ({ context, page }) => {
  // Cookies are added to the context, before or after navigation.
  await context.addCookies([
    {
      name: 'session_id',
      value: 'abc123',
      domain: 'example.com', // no leading dot needed for an exact host
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: 'Lax',
      expires: Math.floor(Date.now() / 1000) + 3600, // unix seconds
    },
  ]);

  await page.goto('https://example.com');

  // Read cookies back. Filter by URL to scope the result.
  const cookies = await context.cookies('https://example.com');
  const session = cookies.find((c) => c.name === 'session_id');

  expect(session?.value).toBe('abc123');
  expect(session?.httpOnly).toBe(true);
});

A few details trip people up. The expires field is in seconds since the Unix epoch, not milliseconds — pass -1 for a session cookie that dies when the context closes. You must supply either a url or the domain+path pair; you cannot supply both url and domain in the same cookie object. And because httpOnly cookies are invisible to JavaScript, context.cookies() is the only reliable way to read them in a test — document.cookie will never show them.

Clearing and selectively removing cookies

To reset state between scenarios, clear cookies. Modern Playwright lets you clear everything or filter by name, domain, or path, which is handy when you want to expire just the auth cookie and keep a consent cookie intact.

test('clear cookies selectively', async ({ context, page }) => {
  await page.goto('https://example.com');

  // Remove every cookie in the context.
  await context.clearCookies();

  // Or remove only matching cookies (filters are ANDed together).
  await context.clearCookies({ name: 'session_id' });
  await context.clearCookies({ domain: 'example.com' });

  const remaining = await context.cookies();
  expect(remaining.find((c) => c.name === 'session_id')).toBeUndefined();
});

Reading and writing localStorage and sessionStorage

Unlike cookies, localStorage and sessionStorage are not exposed through a dedicated Playwright API. They live in the page’s JavaScript context, so you reach them with page.evaluate(). The one rule you must respect: you can only touch storage for an origin after you have navigated to a page on that origin. Calling localStorage.setItem on about:blank throws a security error.

test('seed and read localStorage', async ({ page }) => {
  // Navigate first so the origin exists for storage access.
  await page.goto('https://example.com');

  // Write values. evaluate runs in the browser, not in Node.
  await page.evaluate(() => {
    localStorage.setItem('theme', 'dark');
    localStorage.setItem(
      'user',
      JSON.stringify({ id: 42, plan: 'pro' }),
    );
    sessionStorage.setItem('wizardStep', '3');
  });

  // Read a value back into Node for assertions.
  const theme = await page.evaluate(() => localStorage.getItem('theme'));
  expect(theme).toBe('dark');

  // Parse JSON that you stored as a string.
  const user = await page.evaluate(() =>
    JSON.parse(localStorage.getItem('user') ?? '{}'),
  );
  expect(user.plan).toBe('pro');
});

Remember that storage values are always strings, so serialize objects with JSON.stringify on the way in and parse them on the way out. If you want a clean slate, run localStorage.clear() (or remove a single key with removeItem) inside an evaluate call.

Seeding storage before the app’s scripts run

Sometimes you need a value present before the application’s JavaScript reads it — a feature flag the bundle checks on first paint, for example. Setting it after page.goto() is too late because the app has already booted. The fix is context.addInitScript() (or page.addInitScript()), which injects code that runs in every page of the context before any of the site’s own scripts execute.

test('inject a flag before the app boots', async ({ context, page }) => {
  // Runs on every new document, before the page's own scripts.
  await context.addInitScript(() => {
    window.localStorage.setItem('feature.newCheckout', 'true');
    window.localStorage.setItem('cookieConsent', 'accepted');
  });

  await page.goto('https://example.com/checkout');

  // The app saw the flag during its first render — no consent banner.
  await expect(page.getByTestId('new-checkout')).toBeVisible();
  await expect(page.getByText('We use cookies')).toBeHidden();
});

This pattern is the cleanest way to dismiss cookie-consent overlays and toggle feature flags without ever touching the UI, and it keeps the setup out of your individual test bodies.

Reusing authenticated state with storageState

The most valuable application of Playwright cookies localStorage manipulation is skipping login. Playwright can serialize an entire context — cookies plus localStorage for every origin — into a JSON file via context.storageState(), then rehydrate a fresh context from that file. Log in once in a global setup, save the state, and every test starts already authenticated.

// auth.setup.ts — runs once, before the rest of the suite.
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('s3cret');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Wait for a post-login signal so the session is fully established.
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  // Persist cookies + localStorage to disk.
  await page.context().storageState({ path: authFile });
});

Wire the saved file into your config so projects depend on the setup and load the state automatically. The dependencies key guarantees the setup runs first, and storageState tells every test in that project to start from the saved session.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /auth\.setup\.ts/ },
    {
      name: 'chromium',
      use: { storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});

You can also pass storageState ad hoc when creating a context for a single test, which is perfect for multi-role scenarios where one spec needs an admin session and another needs a regular user.

test('admin sees the audit log', async ({ browser }) => {
  // Build a context from a previously saved admin state file.
  const adminContext = await browser.newContext({
    storageState: 'playwright/.auth/admin.json',
  });
  const page = await adminContext.newPage();

  await page.goto('https://example.com/admin');
  await expect(page.getByRole('link', { name: 'Audit log' })).toBeVisible();

  await adminContext.close();
});

Treat the generated .auth files like secrets: add the folder to .gitignore so real session tokens never land in version control.

🚀 Level Up Your Playwright

From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.

Cookies vs localStorage vs sessionStorage: a quick reference

AspectCookieslocalStoragesessionStorage
Playwright accesscontext.addCookies() / context.cookies()page.evaluate() + addInitScript()page.evaluate()
ScopeBrowser context (all pages)Per originPer origin + per tab
Sent to serverYes, on every matching requestNoNo
Captured by storageStateYesYes (per origin)No
Survives context restartIf not a session cookieYes (via storageState)No
Readable from JSOnly if not httpOnlyAlwaysAlways

The standout row is the last data line about storageState: it captures cookies and localStorage but not sessionStorage, by design, because session storage is meant to be tab-scoped and ephemeral. If an app stuffs a token into sessionStorage, you must re-seed it per context with an init script rather than relying on the saved state file.

Best practices and common pitfalls

  • Navigate before touching web storage. Access localStorage only after page.goto() on the target origin, or use addInitScript() for pre-boot seeding.
  • Use seconds for cookie expiry. The expires field is Unix seconds; -1 means a session cookie.
  • Don’t read httpOnly cookies via document.cookie. Use context.cookies(), which sees httpOnly values.
  • Refresh stale auth files. Saved tokens expire — regenerate storageState in CI rather than committing it.
  • Keep sessionStorage seeding explicit. Because storageState ignores it, re-inject session storage per context when the app depends on it.
  • Isolate per worker. Each context is already isolated, so avoid mutating a shared state file mid-run; create role-specific files instead.

Conclusion

Mastering Playwright cookies localStorage manipulation turns brittle, login-heavy suites into fast, deterministic ones. Cookies live on the context and are handled with addCookies, cookies, and clearCookies; web storage lives in the page and is reached through page.evaluate() and addInitScript(); and storageState ties it all together so you authenticate once and reuse the session everywhere. Adopt these patterns and your tests will start in exactly the state you intend — every single run.

FAQ

Why can’t I set localStorage before page.goto in Playwright?

Web storage is partitioned by origin, and before navigation the page sits on about:blank, which has no real origin to attach storage to — so the browser throws a security error. Navigate to a page on the target origin first, then call page.evaluate() to write storage. If you need a value present before the app’s own scripts run, use context.addInitScript(), which executes on every new document ahead of the site’s code.

Does Playwright’s storageState save sessionStorage?

No. context.storageState() serializes cookies and localStorage for each origin, but it deliberately omits sessionStorage because session storage is scoped to a single tab and is meant to be ephemeral. If your application stores an auth token or wizard state in sessionStorage, re-seed it per context using addInitScript() or a page.evaluate() call after navigation instead of expecting the saved state file to restore it.

How do I read an httpOnly cookie in a Playwright test?

Use context.cookies(), optionally filtered by URL. Because httpOnly cookies are intentionally hidden from JavaScript, document.cookie inside page.evaluate() will never return them. Playwright’s context API reads cookies straight from the browser’s cookie store, so it exposes the name, value, httpOnly, secure, sameSite, and expiry fields even for httpOnly entries that scripts can’t see.

🎓 Master Playwright End to End

Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.