skip to content

Per-Role Credentials

One saved-state file per role picked by project or describe block, a per-worker account keyed off the worker index, and an API request context that carries the same session.

on this pageshow

explore

questions

5

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, 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 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