Every test starts with login. Login page, enter email, enter password, click submit, wait for redirect. That is 5-10 seconds per test. If you have 200 tests, that is 20-30 minutes just on login. Playwright has a better way -- save the authenticated state once, reuse it everywhere.
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
// Log in once
await page.goto('/login');
await page.getByLabel('Email').fill('testuser@example.com');
await page.getByLabel('Password').fill('SecurePass123!');
await page.getByRole('button', { name: 'Login' }).click();
// Wait for successful login
await expect(page).toHaveURL('/dashboard');
// Save the authentication state (cookies, localStorage, sessionStorage)
await page.context().storageState({ path: authFile });
});import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project -- runs first, logs in
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// All tests use the saved auth state
{
name: 'chromium',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'], // Runs after setup
},
],
});import { test, expect } from '@playwright/test';
// No login code needed -- user is already authenticated!
test('view dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
await expect(page.getByText('testuser@example.com')).toBeVisible();
});
test('update profile', async ({ page }) => {
await page.goto('/profile');
await page.getByLabel('Full Name').fill('Test User Updated');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Profile updated')).toBeVisible();
});export default defineConfig({
projects: [
{ name: 'setup-admin', testMatch: /admin\.setup\.ts/ },
{ name: 'setup-user', testMatch: /user\.setup\.ts/ },
{
name: 'admin-tests',
use: { storageState: 'playwright/.auth/admin.json' },
dependencies: ['setup-admin'],
testMatch: /.*admin.*\.spec\.ts/,
},
{
name: 'user-tests',
use: { storageState: 'playwright/.auth/user.json' },
dependencies: ['setup-user'],
testMatch: /.*user.*\.spec\.ts/,
},
],
});If your app supports API-based login, you can skip the UI entirely during setup. This is even faster.
import { test as setup } from '@playwright/test';
setup('authenticate via API', async ({ request }) => {
// Log in via API -- no browser UI needed
const response = await request.post('/api/auth/login', {
data: {
email: 'testuser@example.com',
password: 'SecurePass123!',
},
});
// Save the auth state from the API response
await request.storageState({ path: 'playwright/.auth/user.json' });
});Add playwright/.auth/ to your .gitignore. Auth state files contain session tokens. Never commit them.
Q: How do you handle tests that need to test the login page itself?
A: Create a separate project in playwright.config.ts that does NOT use storageState. Run login-specific tests in that project without any pre-authenticated state. The rest of your tests use the saved auth state. This way, you test login once properly and skip it everywhere else.
Key Point: Save auth state with storageState(), reuse it across all tests. Login once, test everything. This saves minutes per test run.