Q26 of 42 · Playwright
How do you handle authentication state for tests that require different user roles?
Short answer
Short answer: Pre-build a `storageState` JSON per role in a `setup` project that runs first. Each test project (or `test.use()` block) loads the right file. Validate freshness via a setup-time API call; regenerate on token expiry. One login per role per CI run, not per test.
Detail
Multi-role auth in Playwright follows the same caching principle as Cypress's cy.session but uses storageState JSON files.
Step 1: A setup project per role:
// playwright.config.ts
projects: [
{
name: 'setup-admin',
testMatch: /global\.setup\.admin\.ts/,
},
{
name: 'setup-viewer',
testMatch: /global\.setup\.viewer\.ts/,
},
{
name: 'admin-tests',
use: { storageState: 'playwright/.auth/admin.json' },
dependencies: ['setup-admin'],
testMatch: /admin\.spec\.ts/,
},
{
name: 'viewer-tests',
use: { storageState: 'playwright/.auth/viewer.json' },
dependencies: ['setup-viewer'],
testMatch: /viewer\.spec\.ts/,
},
],
Step 2: Setup files that log in via API and save state:
// global.setup.admin.ts
import { test as setup } from '@playwright/test';
setup('authenticate as admin', async ({ request }) => {
await request.post('/api/login', {
data: { email: process.env.ADMIN_EMAIL, password: process.env.E2E_PASSWORD },
});
await request.storageState({ path: 'playwright/.auth/admin.json' });
});
Step 3: Tests just consume the auth:
// admin.spec.ts — runs with admin auth automatically
test('admin can delete users', async ({ page }) => {
await page.goto('/admin/users');
await page.getByRole('button', { name: 'Delete' }).first().click();
});
Step 4: Validate freshness if your tokens expire mid-run. Add a small validation step in setup that re-runs if /api/me returns 401, or schedule the setup project to re-run periodically (daily).
Mid-spec role switching (one test as multiple users): create contexts manually with different storage states.
test('admin and viewer interact', async ({ browser }) => {
const adminCtx = await browser.newContext({ storageState: '.auth/admin.json' });
const viewerCtx = await browser.newContext({ storageState: '.auth/viewer.json' });
// Use independently
});
Watch-outs:
- Don't commit
.auth/*.json— they're credentials. Gitignore them; recreate via setup on every CI run. - Token TTL. Set the validation interval shorter than the token lifetime, or add a validate-and-refresh setup.
- SSO flows. Some can't be hit purely via
request.post— use a one-timepage.goto(loginUrl)flow inside the setup project, then savestorageState.