Skip to main content
Back to Blog
Tutorial
2026-07-14

Playwright localStorage and sessionStorage API Guide for Version 1.61

Use Playwright 1.61 page.localStorage and page.sessionStorage to inspect, seed, clear, and diagnose origin-scoped browser state without page.evaluate boilerplate.

Playwright page reading separate localStorage and sessionStorage key-value stores

Playwright 1.61 introduces page.localStorage and page.sessionStorage, two async WebStorage objects for the page's current origin. Use setItem(), getItem(), items(), removeItem(), and clear() after navigating to the target origin. This is the direct Playwright localStorage and sessionStorage API: it replaces routine page.evaluate() boilerplate, works consistently across browsers, and returns string values or null. It does not make session storage part of storageState. For the surrounding test-runner model, begin with the Playwright E2E complete guide.

This guide focuses on state inspection and setup inside one running test: feature flags, onboarding markers, drafts, cart state, and storage-driven authentication diagnostics. It also explains when older browserContext.storageState() and browserContext.addInitScript() patterns remain the correct tools. The 1.61 release notes call Web Storage a new API; storage itself and Playwright's ability to save local storage are not new.

The Complete Playwright 1.61 WebStorage Surface

Both page properties return the same WebStorage interface. The only difference is which browser store they address. The official WebStorage reference marks every method below as added in 1.61.

MethodResultBest testing useImportant behavior
getItem(name)`Promise<stringnull>`Read one token, preference, or marker
setItem(name, value)Promise<void>Seed or replace one valueValues are strings; existing values are overwritten
items()Array of { name, value }Audit or snapshot all keys for the current originNormalize before order-insensitive comparisons
removeItem(name)Promise<void>Test one missing keyRemoving an absent key is a no-op
clear()Promise<void>Test a first-visit or signed-out storage conditionClears the selected store for the current origin

The properties themselves are also new in 1.61: page.localStorage addresses local storage, while page.sessionStorage addresses session storage. The Page API reference defines each as access to storage for the current origin. That phrase is the main design constraint. Navigate first, then read or write.

items() returns name/value objects rather than a JavaScript Storage instance. That makes the result serializable and keeps test code outside the page execution environment. Convert it to a map with Object.fromEntries(items.map(({ name, value }) => [name, value])) when key-based assertions are clearer.

What Is New and What Is an Older Supported Pattern

The 1.61 API makes direct access concise:

await page.goto('https://app.example.test/settings');
await page.localStorage.setItem('theme', 'dark');
expect(await page.localStorage.getItem('theme')).toBe('dark');

Before 1.61, test code commonly used page.evaluate(() => localStorage.getItem('theme')). That still works and remains necessary for browser APIs not represented by Playwright's public surface, but it is no longer the clearest default for basic Web Storage operations.

Two older APIs solve different jobs and were not replaced:

  • browserContext.storageState() captures cookies and local storage, with optional IndexedDB, across origins represented in the context state.
  • browserContext.addInitScript() runs before application scripts in new documents, so it can preseed storage that must exist before boot code reads it.

The 1.61 page properties act on the current origin after it exists. They do not add a context-wide session-storage persistence format, and they do not retroactively rerun application initialization after a value changes.

Example 1: Seed and Clear localStorage Without page.evaluate

This complete spec is runnable without an application server. A context route fulfills a page at a stable HTTPS origin; the page reads a theme during boot. The test sets local storage through the 1.61 API, reloads to make the application consume it, and then verifies the cleared-state behavior.

// tests/local-storage.spec.ts
import { test, expect } from '@playwright/test';

const origin = 'https://preferences.example.test';

test('controls a persisted theme through page.localStorage', async ({ context, page }) => {
  await context.route(origin + '/**', async route => {
    await route.fulfill({
      contentType: 'text/html',
      body: [
        '<h1>Preferences</h1>',
        '<output data-testid="theme"></output>',
        '<script>',
        "document.querySelector('[data-testid=theme]').textContent = " +
          "localStorage.getItem('theme') || 'light';",
        '</script>',
      ].join(''),
    });
  });

  await page.goto(origin + '/settings');
  await expect(page.getByTestId('theme')).toHaveText('light');

  await page.localStorage.setItem('theme', 'dark');
  expect(await page.localStorage.getItem('theme')).toBe('dark');

  await page.reload();
  await expect(page.getByTestId('theme')).toHaveText('dark');

  await page.localStorage.clear();
  await page.reload();
  await expect(page.getByTestId('theme')).toHaveText('light');
});

The reload is not a timing workaround. This sample application reads local storage once during its boot script, so a reload is the product event that makes the new value observable. A reactive application may listen for another event or expose a settings control; use the real product trigger rather than adding a delay.

When a stored value represents JSON, serialize it explicitly:

await page.localStorage.setItem('preferences', JSON.stringify({ theme: 'dark' }));
const raw = await page.localStorage.getItem('preferences');
expect(raw).not.toBeNull();
expect(JSON.parse(raw!)).toEqual({ theme: 'dark' });

Web Storage stores strings. Passing an object is a TypeScript error in the new API and should remain one; silently coercing an object to [object Object] would make the fixture misleading.

Example 2: Prove sessionStorage Is Page-Scoped

The next runnable spec uses two pages in the same context. It shows the testing difference directly: local storage is visible to both pages at the same origin, while the first page's session storage is not present in the second page. A reload of the first page retains its session value because the page session is still alive.

// tests/session-storage.spec.ts
import { test, expect } from '@playwright/test';

const origin = 'https://editor.example.test';

test('keeps a draft in one page session', async ({ context }) => {
  await context.route(origin + '/**', route =>
    route.fulfill({
      contentType: 'text/html',
      body: '<h1>Editor</h1><output data-testid="draft"></output>' +
        '<script>document.querySelector("[data-testid=draft]").textContent=' +
        'sessionStorage.getItem("draft") || "empty"</script>',
    }),
  );

  const first = await context.newPage();
  await first.goto(origin + '/draft');
  await first.localStorage.setItem('workspace', 'quality');
  await first.sessionStorage.setItem('draft', 'release notes');

  await first.reload();
  await expect(first.getByTestId('draft')).toHaveText('release notes');

  const second = await context.newPage();
  await second.goto(origin + '/draft');

  expect(await second.localStorage.getItem('workspace')).toBe('quality');
  expect(await second.sessionStorage.getItem('draft')).toBeNull();
  await expect(second.getByTestId('draft')).toHaveText('empty');
});

This is why two pages are not interchangeable with two contexts. Pages in one context can share origin-local state and cookies, yet each page has its own session storage. For completely independent users, use separate contexts as described in the BrowserContext guide.

Inspect State Without Coupling to Item Order

Use items() when the set of keys matters more than one value. Convert the array into a deterministic representation before asserting:

await page.localStorage.setItem('locale', 'en-GB');
await page.localStorage.setItem('theme', 'dark');

const items = await page.localStorage.items();
const state = Object.fromEntries(items.map(({ name, value }) => [name, value]));

expect(state).toEqual({ locale: 'en-GB', theme: 'dark' });

The official API promises an array of name/value pairs. It does not make array position part of the testing contract, so avoid assertions against items()[0] unless item order is independently meaningful to the application. A map also produces a more readable failure when one key is absent or has the wrong serialized value.

Do not dump entire stores into public logs by default. Authentication tokens, user identifiers, feature entitlements, and drafts can all live in Web Storage. Select and redact the fields needed for diagnosis, especially in CI traces and error attachments.

Preload sessionStorage Before Application Code Runs

Direct page.sessionStorage.setItem() requires a current origin. If the application reads session storage during its first script and immediately redirects, setting a value after page.goto() is too late. The established solution is still browserContext.addInitScript().

Playwright's authentication guide explicitly says storageState does not persist session storage and demonstrates saving it separately, then restoring it with an init script. In 1.61, items() makes capture cleaner while the restore timing remains the same:

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

const origin = 'https://workspace.example.test';

test('restores session state before boot', async ({ browser }) => {
  const source = await browser.newContext();
  await source.route(origin + '/**', route => route.fulfill({
    contentType: 'text/html',
    body: '<h1>Workspace</h1>',
  }));
  const sourcePage = await source.newPage();
  await sourcePage.goto(origin);
  await sourcePage.sessionStorage.setItem('activeProject', 'apollo');

  const items = await sourcePage.sessionStorage.items();
  const saved = Object.fromEntries(items.map(({ name, value }) => [name, value]));
  await source.close();

  const restored = await browser.newContext();
  await restored.addInitScript(
    ({ hostname, storage }) => {
      if (window.location.hostname === hostname) {
        for (const [name, value] of Object.entries(storage))
          window.sessionStorage.setItem(name, value);
      }
    },
    { hostname: new URL(origin).hostname, storage: saved },
  );
  await restored.route(origin + '/**', route => route.fulfill({
    contentType: 'text/html',
    body: '<output data-testid="project"></output>' +
      '<script>document.querySelector("[data-testid=project]").textContent=' +
      'sessionStorage.getItem("activeProject") || "none"</script>',
  }));

  const page = await restored.newPage();
  await page.goto(origin);
  await expect(page.getByTestId('project')).toHaveText('apollo');
  await restored.close();
});

Restrict the init script by hostname or exact origin. An unrestricted restore can seed a token or flag into every domain the context visits, including third-party pages. If the stored state is authentication material, protect the separate file with the same controls used for cookie state.

WebStorage Versus storageState

These APIs are complementary, not competing aliases.

GoalUseIncludes sessionStorage?Timing and scope
Read or mutate one current page originpage.localStorage / page.sessionStorageDirectly, when selectedAfter the page has a target origin
Save reusable authenticated browser statecontext.storageState()NoCaptures cookies and local storage; IndexedDB is optional
Initialize a new context from saved statebrowser.newContext({ storageState })NoBefore pages are created
Replace state in an existing contextcontext.setStorageState()NoSupported since 1.59, not new in 1.61
Seed session data before boot scriptscontext.addInitScript()ManuallyRuns in new documents before application scripts

The BrowserContext API defines storageState() as cookies, local storage, and optional IndexedDB. The authentication guide separately documents that session storage needs manual persistence. The presence of page.sessionStorage.items() in 1.61 does not alter that file format.

Use direct page storage for surgical setup and assertions. Use storage state for reusable login or broad context initialization. Prefer a product API or UI action when storage is only an implementation detail and the behavior can be established naturally; direct storage setup is strongest when the state itself is the scenario.

Define the Storage Contract Before Seeding Values

Direct access makes setup easy enough that a suite can accidentally encode undocumented keys everywhere. Before adding a call to setItem(), record five facts: which origin owns the key, whether its lifetime is local or page-session, how the value is serialized, what missing or malformed data means, and which product action removes it. Put repeated setup behind a small domain helper only after those facts are stable.

ScenarioInitial storeUser-visible assertionDefect it can expose
First visitKey absentDefault experience appearsApp assumes a value always exists
Returning userValid current valueSaved preference or progress returnsReader ignores or misparses persisted state
Schema upgradeOlder versioned JSONApp migrates or safely falls backDeployment breaks existing browser profiles
Corrupt valueInvalid or incomplete stringRecovery path appears without a crashUnhandled parse or validation error
Logout/resetAuth or preference keys initially presentProduct action removes only intended dataSensitive state survives logout or unrelated preferences are erased

Keep one test that creates the value through the real product UI and verifies the resulting behavior. Playwright's best-practices guidance recommends testing what users see instead of relying on implementation details. Direct WebStorage setup then earns its place in focused edge tests because it reaches difficult states without repeating a long journey.

Avoid helpers named only seedStorage that accept an arbitrary object. A helper such as setOnboardingVersion(page, 3) can validate origin, key, and serialization; a generic bag silently spreads typos and obsolete keys. Return to the page through an observable product event and assert the outcome, not just the stored string. Otherwise the test proves Playwright wrote a key but says nothing about whether the application uses it correctly.

Authentication values deserve stricter handling. Prefer an official login API plus storageState when possible. If a test must seed a token directly, obtain a short-lived test credential from controlled infrastructure, never hard-code it, and avoid printing items() wholesale on failure. A malformed-value test should use synthetic data that cannot authenticate anywhere.

Failure Diagnosis

page.localStorage is undefined or is not in the TypeScript type

The test is resolving Playwright older than 1.61. Run pnpm exec playwright test --version from the same package, inspect lockfile resolution, and reinstall matching browser binaries. Do not replace the new call with as any; either upgrade or use the older page.evaluate() pattern intentionally.

getItem returns null even though DevTools showed a value

Check the current URL's scheme, host, and port. Web Storage is origin-scoped, and the page API addresses the current origin. A value at https://app.example.test is not the value at http://app.example.test, another port, or an identity subdomain. Also confirm the exact key and that a navigation did not move the page before the read.

setItem succeeds but the UI does not change

The storage write and application state are separate. Many apps read storage only during boot or when a settings event fires. Reload, navigate, or invoke the product's real refresh action, then use a web-first assertion. Do not add waitForTimeout; elapsed time cannot cause code that never rereads storage to run.

sessionStorage exists after reload but disappears in a second page

That is the expected page-session boundary, not flakiness. Keep the scenario in one page when it models a tab-scoped draft. Use addInitScript to initialize each new page deliberately, or use local storage/context state only if that matches the product design.

storageState restores localStorage but not sessionStorage

That is also expected. The official auth guide says Playwright does not persist session storage in storage state. Save selected session values separately and restore them with an origin-restricted init script. Never change the storage-state JSON shape and assume Playwright will consume an invented sessionStorage field.

A JSON assertion receives a string

Web Storage values are strings. Parse a known JSON value explicitly and fail with context when parsing is invalid. Do not globally parse every item; flags such as true, plain tokens, and version strings may not share one serialization contract.

A cross-origin iframe has different storage

page.localStorage and page.sessionStorage target the page's current origin, not every frame origin. If storage inside an embedded origin is truly part of the product contract, work through that frame and its UI or use a narrowly scoped frame evaluation. Keep third-party origins mocked or controlled, following Playwright's recommendation not to test dependencies you do not own.

Version Scope and Limitations

All five WebStorage methods and both Page properties require Playwright 1.61 or newer. The 1.61 release notes list Chromium 149, Firefox 151, and WebKit 26.5 as bundled browser versions and describe the API as browser-consistent. storageState, addInitScript, and browser contexts are older supported patterns; context.setStorageState() arrived in 1.59.

The direct API covers only local and session Web Storage. It does not expose IndexedDB, Cache Storage, service-worker caches, cookies, or passkeys through the same object. Use storageState({ indexedDB: true }) where documented for IndexedDB-backed authentication, BrowserContext cookie methods for cookies, and the sibling Playwright 1.61 passkey guide for context.credentials.

Current-origin access is deliberately narrow. Navigate before use, account for redirects that change origins, and do not assume setting a value dispatches an application event. The API is a test-control surface, not a substitute for validating the user's settings UI. Keep at least one end-to-end path that writes preferences through the product and then use direct storage calls for focused edge states.

For reusable authoring and browser inspection guidance, browse QA automation skills and the Playwright CLI skill. Pair storage setup with stable Playwright locators so failures identify state boundaries instead of selector noise. The older authentication-state guide is useful when storage represents multiple signed-in roles rather than one page-level flag.

Frequently Asked Questions

Is page.localStorage new in Playwright 1.61?

Yes. The Page property and the WebStorage methods getItem, setItem, items, removeItem, and clear are marked as added in 1.61. Local storage support through evaluation and storageState existed earlier.

Does page.sessionStorage persist through a reload?

It remains part of that page session, so a normal reload can read the same value. A separate page does not automatically receive it. Your app may clear or overwrite the value during boot, which is application behavior rather than a Playwright lifecycle rule.

Does Playwright storageState include sessionStorage in 1.61?

No. The new direct session-storage API did not change storageState. Playwright's official authentication guide still requires separate capture and an init script when session storage must be reused.

Should I replace every page.evaluate localStorage call?

Replace straightforward get, set, list, remove, and clear operations because the 1.61 API is clearer and typed. Keep evaluation when the test intentionally executes more complex application-side logic or accesses a frame/browser API not exposed by WebStorage.

Can setItem accept an object or boolean?

No. Its documented value parameter is a string. Use JSON.stringify for structured values and parse them explicitly when reading. For flags, match the application's real encoding, such as 'true' or '1'.

Why must the page navigate before I use WebStorage?

The API addresses storage for the current origin. A test must establish the scheme, host, and port whose store it intends to manipulate. Navigating first also prevents accidentally seeding an initial or redirected origin.

Does changing localStorage automatically update a React or Vue UI?

Not necessarily. The browser store changes, but application memory may not. Trigger the product behavior that rereads the value, commonly a reload or navigation, and assert the user-visible result. Avoid arbitrary sleeps.

When should I use clear instead of a fresh BrowserContext?

Use clear() when the scenario specifically transitions one origin's selected store from populated to empty. Use a fresh context when the test needs a genuinely clean browser profile, including cookies, permissions, other origins, and unrelated storage mechanisms.