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);
});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.