
Imagine a regression suite of 200 tests, and every one of them starts by opening the login page, typing a username, typing a password, and waiting for the dashboard. Each login costs a few seconds, so the suite wastes minutes per run. Then the login page changes and half the suite fails for a reason that has nothing to do with the feature under test.
Playwright Automation solves this with a simple idea: log in once, save the session, and reuse it. This Playwright authentication tutorial shows how to automate login in Playwright, how to handle different user roles, and how to manage sessions so your tests are fast and stable.
Almost every real application sits behind a login. If each test logs in through the UI, you pay three costs:
The better pattern is to test the login flow thoroughly in a few dedicated tests, then let all other tests start already authenticated.
Start with the simplest version. This is the Playwright login testing you will write first:
tsimport { test, expect } from '@playwright/test';
test('user can log in with valid credentials', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill(process.env.TEST_EMAIL!); await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!); await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard/); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();});
Two habits are worth building from day one. First, use role- and label-based locators, which survive styling changes better than CSS selectors. Second, keep credentials in environment variables, never in the test file or the repository.
Also test the failure path, since invalid credentials are part of the login feature:
tstest('shows an error for a wrong password', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('[email protected]'); await page.getByLabel('Password').fill('wrong-password'); await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid email or password')).toBeVisible();});
Playwright can save the browser’s authenticated state to a file and load it into later tests. The saved state includes cookies and local storage, so the new test starts logged in without touching the login page.
ts// Save after logging inawait page.context().storageState({ path: 'playwright/.auth/user.json' });ts// Reuse in a test filetest.use({ storageState: 'playwright/.auth/user.json' });
Add the playwright/.auth folder to .gitignore. The file contains live session data and must never be committed.
The recommended approach is a dedicated setup project that runs before your main tests. Create tests/auth.setup.ts:
tsimport { 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.TEST_EMAIL!); await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!); await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL(/dashboard/); await page.context().storageState({ path: authFile });});Then wire it up in playwright.config.ts:tsimport { 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'], }, ],});
The dependencies line makes Playwright run the setup first. Every test in the chromium project then starts authenticated, and the login happens once per run instead of once per test.
For tests that must start logged out, such as the login tests above, override the state:
tstest.use({ storageState: { cookies: [], origins: [] } });
Most applications have more than one kind of user, such as an admin and a regular member. Save one state file per role:
|
Role |
State file |
Used for |
|
Admin |
playwright/.auth/admin.json |
User management, settings |
|
Member |
playwright/.auth/member.json |
Everyday workflows |
|
Logged out |
none |
Login, signup, public pages |
Create one setup step per role, then choose the file per test file or per project:
tstest.describe('admin area', () => { test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin sees the user list', async ({ page }) => { await page.goto('/admin/users'); await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); });});
If your tests modify server-side data for the logged-in account, parallel workers can interfere with each other. In that case, give each worker its own account using a worker-scoped fixture, so tests stay independent.
Logging in through the UI is slow even once. If your application has a login endpoint, authenticate with Playwright’s request context and save the resulting state:
tsimport { test as setup } from '@playwright/test';
setup('authenticate via API', async ({ request }) => { const response = await request.post('/api/auth/login', { data: { email: process.env.TEST_EMAIL, password: process.env.TEST_PASSWORD, }, }); if (!response.ok()) throw new Error(`Login failed: ${response.status()}`);
await request.storageState({ path: 'playwright/.auth/user.json' });});
This works when the server sets a session cookie. If your app returns a token that the frontend stores itself, you will need to place that token into local storage or headers manually. Keep at least a few UI login tests so the real login form stays covered.
These are the problems that most often break Playwright session handling:
AI-Powered Playwright Automation is now a practical part of many teams’ routines. Recent Playwright releases include tooling for AI assistants, such as an MCP server and test-generation agents, though the exact features change between versions, so check the current Playwright documentation before relying on them.
AI works best on the repetitive parts of authentication testing:
Treat the output as a draft. AI-generated tests can pass for the wrong reason, use brittle selectors, or hard-code credentials. Review every generated test, run it several times to check stability, and make sure it asserts something meaningful, not just that a page loaded.
Log in once in a setup project, save the session with storageState, and configure your test projects to load that file.
A folder such as playwright/.auth works well. Add it to .gitignore because it contains session data.
Yes, but the practical options are disabling MFA for test accounts in a test environment or generating TOTP codes from a stored secret. Avoid automating real SMS or email delivery.
The usual causes are an expired session, a token stored in session storage that was not restored, or another test changing the same account. Check each of these in turn.
It is useful for drafting tests and finding gaps, but it needs human review. Login flows are security-sensitive, so verify assertions and credential handling yourself.
Good Playwright test automation treats login as infrastructure, not as a step to repeat. This Playwright authentication tutorial covered the full path: test the login form properly in a few focused tests, use a setup project and storageState for everything else, and handle roles, API login, and session pitfalls deliberately. The result is a suite that runs faster and fails only when something real breaks.
For your next step, take one existing test suite this week, move its login into a setup project, and compare the total run time before and after.
If you were improving your own suite, which would you tackle first: moving to storageState for speed, or adding multiple user roles for better coverage, and why?
Follow NareshIT for more practical insights on technology, skills, and career development.