Authentication and State
Most flows start after login. Logging in through the UI in every test is slow and hammers the login endpoint. Playwright's answer: log in once, save the browser state (cookies and local storage) to a file, and start every test with that state loaded. This is the closest thing the UI world has to JMeter's correlation: the session token has to get from the login response into every later request, and here the browser does the carrying.
How It Works
setup project ──logs in──▶ playwright/.auth/user.json
│
chromium project ──loads state──▶ every test starts logged in
- A setup project runs first. It contains one test that logs in through the UI and calls
storageState()to save cookies and local storage to a JSON file. - The chromium project declares
dependencies: ['setup']andstorageState: 'playwright/.auth/user.json'. Every test in it starts with that file loaded into a fresh context.
Tests still get their own isolated context. They just start it from a saved snapshot instead of empty.
Steps
1. Ignore the State File
The file contains session cookies. It must never be committed.
mkdir -p playwright/.auth
echo 'playwright/.auth' >> .gitignore
The init command's .gitignore already has this line.
2. Write the Setup Test
playwright/sample/tests/auth.setup.ts:
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import users from './data/users.json';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('Username').fill(users.standard.username);
await page.getByPlaceholder('Password').fill(users.standard.password);
await page.getByRole('button', { name: 'Login' }).click();
// Wait until the app is really logged in before saving state.
await page.waitForURL('**/inventory.html');
await expect(page.getByText('Products')).toBeVisible();
await page.context().storageState({ path: authFile });
});
The two lines before storageState() matter. If you save too early, before the app has finished setting its cookies, the file is empty and every test starts logged out.
3. Wire the Projects
In playwright.config.ts:
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
testMatch keeps the setup file out of the normal test list. dependencies guarantees it runs first, and that if it fails nothing else runs.
4. Run
npx playwright test
The report shows [setup] › auth.setup.ts › authenticate first, then the chromium tests. npx playwright test --list shows the same order.
Verify: playwright/.auth/user.json exists and contains a cookies array with the app's session cookie.
Tests That Must Start Logged Out
The login tests themselves need a clean session. Override the state for that file:
// tests/login.spec.ts
test.use({ storageState: { cookies: [], origins: [] } });
That is an empty state, so the tests in this file start on the login page like a new visitor. The sample project does exactly this.
More Than One Role
Save one file per role in the setup project, then choose per file or per describe:
// tests/auth.setup.ts
setup('authenticate as admin', async ({ page }) => {
// ... log in as admin ...
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});
setup('authenticate as user', async ({ page }) => {
// ... log in as user ...
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// tests/admin-settings.spec.ts
test.use({ storageState: 'playwright/.auth/admin.json' });
Leave storageState out of the project use block when you do this, and set it per spec file instead.
When Tests Change Shared State
If every test uses the same account and they all edit the same record, they will collide when run in parallel. Two options:
- Best: each test creates its own data (via API in a fixture) and does not touch shared records.
- Fallback: one account per worker. Playwright's docs show a fixture that logs in once per worker using
test.info().parallelIndexto pick an account and saveplaywright/.auth/${index}.json. Use this when the app cannot create data on demand.
Other Kinds of State
| Need | How |
|---|---|
| Start with specific cookies | context.addCookies([...]) in a fixture |
| Start with local storage values | Included in storageState automatically |
| Session storage | Not saved by storageState. Set it with page.addInitScript() from a value you captured |
| IndexedDB | storageState({ indexedDB: true }) since 1.51 |
| A fresh session for one test | test.use({ storageState: { cookies: [], origins: [] } }) |
Recording While Logged In
The recorder can save and reuse state too. Log in once by hand and save:
npx playwright codegen --save-storage=playwright/.auth/user.json https://www.saucedemo.com/
Then record further flows already logged in:
npx playwright codegen --load-storage=playwright/.auth/user.json https://www.saucedemo.com/inventory.html
Tips
-
Assert you are logged in before saving. The most common bug in setup files is saving an empty session.
-
Sessions expire. If tests start failing at the first authenticated step after a long idle, the saved state is stale. The setup project re-runs on every
npx playwright test, so this mostly bites when you run a single file with--no-deps. -
Never commit
.auth/. Treat it like a password file. The same goes for--save-storageoutput from the recorder. -
Log in via API when you can. If the app has a login endpoint, calling it with the
requestfixture and setting the cookie is faster than driving the form. Keep one UI login test so the form itself stays covered.