🎯 What You Will Learn
Authenticate once, protect storage-state files, configure setup dependencies, and compose reusable test fixtures.
- Create a dedicated authentication setup project
- Reuse protected storage state across tests
- Choose shared or per-worker test accounts
- Build typed fixtures with reliable teardown
Choose an Authentication Strategy
A shared account works when parallel tests do not modify conflicting server state. Tests that change data should receive independent accounts or reset their data.
| Strategy | Use it when |
|---|---|
| One shared state | Tests are read-only or independent on the server |
| Per-worker account | Parallel workers modify server-side state |
| Per-test login | Authentication behavior itself is under test |
Protect Authentication Files
Create the recommended auth directory and ignore it before a setup test writes cookies and local storage.
playwright/.auth/Session secret: A valid storage-state file can impersonate the test user. Never commit or attach it to a public issue.
Create the Setup Test
Authenticate through the UI, wait for the final signed-in state, then save the context storage.
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('button', { name: /account/i })).toBeVisible();
await page.context().storageState({ path: authFile });
});Configure the Setup Dependency
The browser project depends on setup, so authentication completes before application tests run.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
use: { baseURL: 'http://127.0.0.1:3000' },
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});Write an Authenticated Test
The page fixture starts with the saved storage state, so the test can navigate directly to a protected route.
import { test, expect } from '@playwright/test';
test('shows the signed-in account', async ({ page }) => {
await page.goto('/account');
await expect(page.getByRole('heading', { name: 'Your account' })).toBeVisible();
});Create a Typed Fixture
Fixtures package repeated setup with teardown. Keep the value focused and release resources after use.
import { test as base, type Page } from '@playwright/test';
type Fixtures = {
accountPage: Page;
};
export const test = base.extend<Fixtures>({
accountPage: async ({ page }, use) => {
await page.goto('/account');
await use(page);
},
});
export { expect } from '@playwright/test';Keep Parallel Tests Independent
If tests modify shared orders, settings, or messages, allocate a unique account per worker or test. Use testInfo.parallelIndex to select or create an identity.
Isolation is not cleanup: A fresh browser context does not reset records stored by your backend. Design test data and account allocation separately.
Recover from Expired State
When the setup state expires, run the setup project again or let the dependency recreate it. Verify the final signed-in UI before saving the new state.
npx playwright test --project=setup
npx playwright test --project=chromiumRecap
- Choose shared or isolated accounts based on server-side behavior.
- Keep storage state and credentials out of Git.
- Use project dependencies for visible, traceable authentication setup.
- Fixtures should be typed, focused, and responsible for teardown.
Checkpoint: You have completed the workflow and have a repeatable reference for your next Playwright project.