Q26 of 42 · Playwright

How do you handle authentication state for tests that require different user roles?

PlaywrightMidplaywrightauthstorage-staterbacmid

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-time page.goto(loginUrl) flow inside the setup project, then save storageState.

// WHAT INTERVIEWERS LOOK FOR

Setup project per role + dependencies + storageState JSON pattern, and the `browser.newContext({ storageState })` for in-test role switching. Bonus for the validate-and-refresh discipline.

// COMMON PITFALL

Logging in once at `globalSetup` time and forgetting that long suites outlast token TTL — silent 401s mid-suite.