level 03 / authentication

Authentication & Sessions

storageState, setup projects, and multi-role auth — log in once, reuse everywhere, and keep your suite fast.

The problem with logging in per test

A UI login takes 3–10 seconds. At 500 tests, that’s up to 80 minutes of CI time spent on a flow you’ve already proven works. Worse: every test now depends on the login page, so one auth hiccup fails the entire suite.

The fix: authenticate once, save the session, and have every test start already logged in.

storageState: the mechanism

// After logging in, capture cookies + localStorage to a file
await page.context().storageState({ path: 'playwright/.auth/user.json' });

// Any new context created with that file starts authenticated
const context = await browser.newContext({
  storageState: 'playwright/.auth/user.json',
});

The setup project pattern

The official pattern: a setup project logs in first, all test projects depend on it and reuse the saved state.

import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  // Confirm login actually worked before saving state
  await expect(page.getByText('Products')).toBeVisible();

  await page.context().storageState({ path: authFile });
});
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});
import { test, expect } from '@playwright/test';

// No login code — the page fixture is already authenticated
test('inventory loads for logged-in user', async ({ page }) => {
  await page.goto('/inventory.html');
  await expect(page.locator('.inventory_item')).toHaveCount(6);
});
Key insight: Add playwright/.auth/ to .gitignore. Session files contain live credentials in cookie form — they must never reach the repository.

Multiple roles

// auth.setup.ts — one setup test per role
setup('authenticate as admin', async ({ page }) => {
  // ...admin login...
  await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});

setup('authenticate as customer', async ({ page }) => {
  // ...customer login...
  await page.context().storageState({ path: 'playwright/.auth/customer.json' });
});
// Per-file role selection
test.use({ storageState: 'playwright/.auth/admin.json' });

test('admin can see the moderation queue', async ({ page }) => { /* ... */ });
// Both roles in ONE test — two contexts
test('customer order appears in admin panel', async ({ browser }) => {
  const customer = await browser.newContext({ storageState: 'playwright/.auth/customer.json' });
  const admin = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });

  const shopPage = await customer.newPage();
  const adminPage = await admin.newPage();
  // customer places order → admin sees it
});

API-based login: skip the UI entirely

setup('authenticate via API', async ({ request }) => {
  // POST credentials straight to the auth endpoint
  await request.post('/api/login', {
    data: { username: 'standard_user', password: 'secret_sauce' },
  });
  // The request fixture carries cookies — save them
  await request.storageState({ path: 'playwright/.auth/user.json' });
});

Faster and more stable than UI login. Use it when the auth API is accessible; keep exactly one UI test that proves the login form itself works.

When sessions expire mid-suite

Token TTL shorter than suite runtime causes late-suite auth failures. Options: re-run setup on a schedule, refresh tokens in a fixture, or extend TTL in the test environment. Detect it first: auth failures clustered at the end of long runs are the signature.

Reference

context.storageState({ path })
Save cookies + localStorage
browser.newContext({ storageState })
Start context authenticated
test.use({ storageState })
Per-file session selection
dependencies: ['setup']
Run auth project before tests
request.storageState({ path })
Save session from API login

Interview questions