skip to content

In Playwright, what does context.storageState({ path }) write, and how do other tests reuse it?

level: juniorimportance: must knowfreq 76%

answer

  1. JSON file, not a browser profile
  2. Cookies plus per-origin storage
  3. One option, three altitudes
  4. In place before the first navigation
  5. path writes it; the call returns it

basics

~20 s

Playwright's context.storageState({ path }) writes a JSON file of that context's cookies and per-origin localStorage. Other tests reuse it by naming the path as storageState in the config, in test.use, or when creating a context.

solid answer

~40 s

`context.storageState({ path: 'admin-state.json' })` serialises everything a browser context currently holds that identifies a session: its cookies, and the `localStorage` entries of every origin the context has visited. It writes that as JSON and also returns it as an object. Three places consume it. In `playwright.config.ts`, `use: { storageState: 'admin-state.json' }` — set once at the top level or on a single project — gives every test in scope a context pre-populated from the file. `test.use({ storageState: 'admin-state.json' })` at the top of a spec file, or inside a `describe`, does the same for that file or block only. In the library API, `browser.newContext({ storageState: 'admin-state.json' })` builds one context from it. In every case the state is in place before the test's first navigation, so the very first request already carries the session.

code

typescript · 11 lines
typescript
import { test, expect } from '@playwright/test';

test('save the admin console session', async ({ page, context }) => {
  await page.goto('/admin/sign-in');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill(process.env.CONSOLE_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  await context.storageState({ path: 'admin-state.json' });
});

go deeper

for a junior

Recall the three-part shape: one call writes the JSON, the config or test.use names the path, and each test's context starts pre-populated. Know that cookies and localStorage are what travel.

for a middle

Explain that each test still gets a fresh context and Playwright seeds it at creation time, and that the same storageState option resolves at config, project, file and newContext altitude with the narrowest winning.

for a senior

Show that you treat the state file as a build artefact produced by a run and consumed by it, and that you can reason from the file's contents when a suite behaves as though it were signed out.

for a principal

Own the call about which layer of the suite declares the session: a top-level config value that colours every test, a per-project value, or a per-file one, and what that choice costs when the suite grows.

## The one call that produces the file `context.storageState()` asks a live browser context for everything it is currently holding that would let a server recognise the session, and hands it back as a plain JavaScript object. Give it a `path` and Playwright also writes that object to disk as JSON: ```ts await context.storageState({ path: 'admin-state.json' }); ``` Two things come out of the call: the **return value** (the state object) and, when `path` is given, the **file**. The file is ordinary JSON — you can open it in an editor, diff it, and read every value in it. ## What it captures - **Cookies** the context holds, each with `name`, `value`, `domain`, `path`, `expires`, `httpOnly`, `secure` and `sameSite`. - **`localStorage`** for every origin the context has visited, grouped in an `origins` array that pairs an origin string with a list of name/value entries. - **IndexedDB**, but only if you opt in with `context.storageState({ path, indexedDB: true })` — available in current Playwright releases, including 1.63. Nothing else travels. There is no page, no navigation history, no in-memory JavaScript state, and no `sessionStorage`. ## The three places the file is read back The same `storageState` option appears at three altitudes, and they differ only in how much of the suite they cover: | Where you set it | Syntax | What it affects | |---|---|---| | Config, top-level `use` | `use: { storageState: 'admin-state.json' }` | every test in the run | | Config, one project's `use` | `projects: [{ name: 'staff', use: { storageState: 'admin-state.json' } }]` | tests under that project | | A spec file or a `describe` | `test.use({ storageState: 'admin-state.json' })` | that file or that block | | The library API | `browser.newContext({ storageState: 'admin-state.json' })` | the one context you create | The narrower setting wins: a `test.use` in a spec file overrides whatever the project supplied for the tests in that file. ## What "restore" actually means Restoring is not a replay of the sign-in flow, and it is not a browser profile being reopened. Each test still gets a brand-new context; Playwright simply seeds that context from the file, so that by the time the test's first `page.goto()` runs, the cookies are in the jar and each origin's `localStorage` entries are present. The application's first request therefore arrives already authenticated, and the app renders the signed-in view without a login form ever being drawn. Because seeding happens at context creation, the sequence in a test is unremarkable: 1. Playwright creates the context and applies the saved state. 2. The test navigates to a page in the internal admin console. 3. The server sees a valid session cookie and serves the dashboard. ## A concrete shape for an admin console suite For an internal admin console with several staff roles, the flow that most suites settle on is: 1. One signed-in run happens somewhere before the suite — it drives the real sign-in form once and ends with `await context.storageState({ path: 'admin-state.json' })`. 2. `playwright.config.ts` points the console's tests at that file with `use: { storageState: 'admin-state.json' }`. 3. Every test in that scope opens straight onto an authenticated page. The file is a build artefact, not source: it is produced by a run and consumed by the tests in the same run. ## Reading the state as an object instead `path` is optional. Called bare, `await context.storageState()` returns the state object and writes nothing: ```ts const state = await context.storageState(); const another = await browser.newContext({ storageState: state }); ``` Both the config option and `browser.newContext` accept either form — a path string, or the object a previous `storageState()` call returned. The path form is the one that crosses a process boundary, because a second process can read a file but cannot see your variable. ## Where the boundaries are Two limits catch people out early. First, the state is **per origin**: the `origins` array is keyed by exact origin, so a file captured against one host, scheme or port does not hand its `localStorage` to a different one. Second, the file is a **snapshot**: it records the values that existed at the moment of the call, so a token that the application writes after that moment is simply absent. Both are properties of the file's shape rather than of the option, and both are visible by opening the JSON and reading it.

  • If the config already sets storageState, what does a test.use in one spec file do to it?
    The narrower setting wins. A file-level or describe-level `test.use({ storageState })` replaces the project's value for the tests in that scope, leaving every other file on the config's value. It is the same option resolved at a tighter altitude, not an additional state merged on top.
  • Does calling context.storageState() without a path do anything useful?
    Yes — it returns the state object and writes no file. That object can be passed straight to `browser.newContext({ storageState: state })` or to `test.use`, which keeps the session in memory when only one process needs it. Pass `path` when a different process must read it.

It is a photocopy of a signed-in session's badge and desk contents, handed to each fresh browser context before it starts work, rather than a recording of the sign-in itself.

saying these in an interview costs you the question

  • Thinking the file is a saved browser profile or user-data directory
  • Believing the state file replays the login form during each test
  • Assuming storageState captures the page or its navigation history
  • Saying the file is applied only after the first page load
  • Thinking only cookies are saved and localStorage never is