skip to content

Signed-In State

The plumbing that lets a test start already logged in: saving a signed-in context to a file, pointing projects at it, and giving each role or each worker its own copy.

on this pageshow

explore

questions

10

In Playwright, how do you run one spec as an admin and another as a read-only staff user?

level: juniorimportance: must knowfreq 68%

answer

  1. One saved session per role
  2. Files grouped under playwright/.auth
  3. Project-level use versus describe-level
  4. Resolved before the page fixture exists
  5. No role switch inside a test

basics

~20 s

Capture one storage-state file per role, then choose the file explicitly: a project per role carrying use.storageState in the config, or test.use with that role's file at the top of a spec or describe block.

solid answer

~40 s

Per-role sign-in is one saved session file per role plus an explicit rule for which file each test loads. Capture `playwright/.auth/admin.json` and `playwright/.auth/viewer.json` once, then select between them at one of two grains. A **project per role** — `use: { storageState: 'playwright/.auth/admin.json' }` — suits whole folders of specs that belong to one identity, and it lets the same folder run twice, once per role, with the role visible in the report as the project name. `test.use({ storageState: 'playwright/.auth/viewer.json' })` at file level or inside a `test.describe` block suits one feature contrasted through two roles side by side. Either way the option resolves while fixtures are built, before the `page` fixture exists, so the first `page.goto` is already authenticated; you cannot call `test.use` inside a test body to switch role mid-test.

code

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

test.describe('billing settings as admin', () => {
  test.use({ storageState: 'playwright/.auth/admin.json' });

  test('admin can reassign the plan owner', async ({ page }) => {
    await page.goto('/settings/billing');
    await expect(page.getByRole('button', { name: 'Change owner' })).toBeEnabled();
  });
});

test.describe('billing settings as viewer', () => {
  test.use({ storageState: 'playwright/.auth/viewer.json' });

  test('viewer sees billing read-only', async ({ page }) => {
    await page.goto('/settings/billing');
    await expect(page.getByRole('button', { name: 'Change owner' })).toBeHidden();
  });
});

go deeper

for a junior

Know that a saved session is a file on disk and that a role is chosen by naming that file, either in a project's use option or with test.use at the top of a spec.

for a middle

Explain when the choice belongs to a project versus a describe block, and why test.use resolves before the page fixture is built rather than during the test.

for a senior

Show how each role file is produced by its own verified capture, and how the suite proves a role's negative permissions instead of only its happy path.

for a principal

Own the cost tradeoff: a project matrix reruns the whole suite once per role and multiplies run time, while a few role-contrast describe blocks are cheap but cover less.

## One session file per role An internal admin console rarely has a single kind of signed-in user. A platform **admin** edits anything, an **editor** changes content but not billing, a **viewer** only reads. Each is a different browser session, so reusing a signed-in state stops being one file and becomes a small set of files with an explicit choice between them. Playwright's representation of a session is a *storage state*: a JSON document holding the cookies and the per-origin web-storage entries captured from a signed-in context. The per-role pattern is that document, once per role, plus a rule saying which one a given test loads. By convention the files sit together: - `playwright/.auth/admin.json` - `playwright/.auth/editor.json` - `playwright/.auth/viewer.json` Nothing in Playwright requires that directory. It is a habit worth keeping because it groups the files as generated artifacts rather than source, and because the role is then readable from the path alone. ## Choosing a role with a project The coarse grain is a project per role. Each project names the role's file under `use`, and a name pattern decides which specs it runs: ```ts // playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /auth\.setup\.ts/ }, { name: 'admin', dependencies: ['setup'], testMatch: /.*\.admin\.spec\.ts/, use: { storageState: 'playwright/.auth/admin.json' }, }, { name: 'viewer', dependencies: ['setup'], testMatch: /.*\.viewer\.spec\.ts/, use: { storageState: 'playwright/.auth/viewer.json' }, }, ], }); ``` This grain also lets the *same* specs run more than once: point two projects at one folder with two different state files and the suite executes once per identity, with no duplicated spec code. ## Choosing a role inside a spec The fine grain is `test.use`, legal at file level and inside a `test.describe` block: ```ts test.describe('billing settings', () => { test.use({ storageState: 'playwright/.auth/viewer.json' }); test('viewer cannot reassign the plan owner', async ({ page }) => { await page.goto('/settings/billing'); await expect(page.getByRole('button', { name: 'Change owner' })).toBeHidden(); }); }); ``` `test.use` applies to every test in its scope no matter where in the block it is written, and it nests — an inner describe's value wins over an outer one. It may not appear inside a test body or a hook, because the option is resolved while the fixtures for that test are being set up, before the `page` fixture is created. That ordering is precisely what makes the first navigation already authenticated. ## Which grain, when | Need | Reach for | Why | |---|---|---| | Whole folders belong to one role | a project per role | routing lives in the config, the report names the role | | One suite exercised as several roles | several projects, one folder | N identities, no duplicated specs | | One feature contrasted across roles | `test.use` per describe | both roles readable in a single file | | A few specs must be anonymous | `test.use({ storageState: { cookies: [], origins: [] } })` | opts out of an inherited session | That last row matters on a console with a signed-in default: a project-level `storageState` is inherited by every spec the project matches, so a public-page test has to opt out on purpose rather than by omission. ## Things that bite - A role file that does not exist fails the run when the context is created, not at the assertion, so produce the files in a setup step instead of assuming they are present. - Two roles must be two accounts. Capturing both from the same login produces two identical files that differ only in name. - One test cannot change identity halfway through. If a scenario genuinely needs both roles at once, open a second context from the other role's file and drive a page from each. - Assert something role-specific early. A viewer spec that only checks the page renders will pass cheerfully while actually running as an admin. - Keep the file name and the account in step: `viewer.json` captured from the editor account is a defect no assertion in the suite will notice.

  • Can one test act as two roles, one after the other?
    Not with one page. The `storageState` option is resolved once per test before fixtures are built, so `test.use` cannot fire mid-test. If a scenario needs both identities at once, open a second context from the other role's file and drive a page from each; the two contexts keep separate cookies.
  • What happens if a spec names a role file that has not been produced yet?
    The run fails when the browser context is created, before the first line of the test body — Playwright reads the file at context creation and errors on a missing path. That is why role captures belong in a setup step that runs ahead of the role projects rather than being assumed present.

saying these in an interview costs you the question

  • Signing in through the login form in every test
  • Calling test.use inside a test body to swap roles
  • One saved state file reused for every role
  • Capturing two role files from the same account
  • Assuming a project's storageState does not reach public-page specs
open as a page

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

level: juniorimportance: must knowfreq 76%

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.

open as a page

What is inside a Playwright storageState JSON file, and which session state does it leave out?

level: middleimportance: must knowfreq 61%

basics

~10 s

A Playwright state file has two arrays: cookies, with each cookie's attributes, and origins, pairing an origin with its localStorage name/value entries. sessionStorage, in-memory JavaScript state and IndexedDB (unless opted in) are absent.

open as a page

In Playwright, how do you make an API request context send calls as the same signed-in role?

level: middleimportance: should knowfreq 36%

basics

~20 s

Seed the API context from that role's saved state file with request.newContext({ storageState }), or issue the calls through page.request, which shares cookies with the page's own context. A sign-in performed in the browser during a test never reaches a separate request context.

open as a page

In Playwright, why can't the storageState option be worker-scoped, and what do you do instead?

level: middleimportance: should knowfreq 45%

basics

~20 s

storageState is a built-in test option, and options are resolved per test so a describe block can change them, which keeps them test-scoped. Add a separate worker-scoped fixture that produces the file path, then override storageState to return it.

open as a page

How do you run one Playwright spec signed out when the project sets storageState for every test?

level: middleimportance: should knowfreq 44%

basics

~20 s

Override the option at file scope. Putting test.use({ storageState: { cookies: [], origins: [] } }) at the top of the spec replaces the project's saved session with an empty one for that file, leaving every other file untouched.

open as a page

How do you give each parallel Playwright worker its own admin account without signing in per test?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Sign in once inside a worker-scoped fixture and key both the account and its saved-state file off test.info().parallelIndex. That index never exceeds the configured worker count, so a fixed pool of accounts covers a run of any size.

open as a page

In Playwright, why might admin.json and viewer.json end up holding the same signed-in session?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Because each capture ran in a context that was already signed in. With a state file supplied under use, the login page redirects straight to the console, the form steps do nothing, and the capture saves the inherited session under a new name.

open as a page

Playwright loads a saved storageState, yet the admin console still renders signed out — how do you diagnose it?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Open the JSON and compare it with the address under test. Usual causes are an origins entry recorded for a different origin, a session kept in sessionStorage which the file never captures, or state saved before the application had written it.

open as a page

When would you pass the object from context.storageState() to browser.newContext instead of a file path?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

Use the object when one process both mints and consumes the session: context.storageState() with no path returns it, and browser.newContext accepts it directly. Use a file path when another process, run or config must read the state.

open as a page