In Playwright, how do you run one spec as an admin and another as a read-only staff user?
answer
- One saved session per role
- Files grouped under playwright/.auth
- Project-level use versus describe-level
- Resolved before the page fixture exists
- No role switch inside a test
basics
~20 sCapture 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 sPer-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 linesimport { 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
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.
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.
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.
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