Answer
Testing JWT-Authenticated Applications in Playwright
Method 1 — Set JWT in localStorage Before Navigation
TypeScript
test('access protected page with JWT', async ({ page }) => {
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
// Set token BEFORE navigating — otherwise the page loads without auth
await page.addInitScript(token => {
localStorage.setItem('access_token', token);
localStorage.setItem('user', JSON.stringify({ id: 1, role: 'admin' }));
}, jwtToken);
await page.goto('/dashboard');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading')).toContainText('Dashboard');
});
Method 2 — Set JWT as Cookie
TypeScript
test('authenticate via JWT cookie', async ({ context, page }) => {
await context.addCookies([{
name: 'jwt_token',
value: 'eyJhbGciOiJIUzI1NiJ9...',
domain: 'localhost',
path: '/',
httpOnly: true,
secure: false, // true for HTTPS
sameSite: 'Lax',
}]);
await page.goto('/dashboard');
await expect(page.getByRole('heading')).toHaveText('Dashboard');
});
Method 3 — Intercept Requests and Add Auth Header
TypeScript
test('add JWT to all API calls', async ({ page }) => {
const token = process.env.TEST_JWT_TOKEN!;
await page.route('/api/**', async route => {
const headers = {
...route.request().headers(),
'Authorization': `Bearer ${token}`,
};
await route.continue({ headers });
});
await page.goto('/products');
await expect(page.locator('.product-list')).toBeVisible();
});
Method 4 — Login via API to Get Real JWT
TypeScript
import { test, expect } from '@playwright/test';
test.beforeAll(async ({ request }) => {
// Login via API to get a real JWT
const response = await request.post('/api/auth/login', {
data: {
email: 'test@example.com',
password: 'password123',
},
});
const { accessToken, refreshToken } = await response.json();
process.env.ACCESS_TOKEN = accessToken;
process.env.REFRESH_TOKEN = refreshToken;
});
test('uses real JWT from API login', async ({ page }) => {
await page.addInitScript(token => {
localStorage.setItem('access_token', token);
}, process.env.ACCESS_TOKEN);
await page.goto('/profile');
await expect(page.locator('.user-name')).toBeVisible();
});
Method 5 — storageState for JWT Persistence
TypeScript
// auth.setup.ts — save JWT in storageState
import { test as setup } from '@playwright/test';
setup('save auth state', async ({ page }) => {
// Login normally via UI
await page.goto('/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
// JWT is now in localStorage — save it
await page.context().storageState({ path: '.auth/user.json' });
});
TypeScript
// playwright.config.ts — all tests start with JWT in localStorage
projects: [
{ name: 'setup', testMatch: '*.setup.ts' },
{
name: 'e2e',
use: { storageState: '.auth/user.json' }, // JWT already in storage
dependencies: ['setup'],
},
],
Verify JWT in Test
TypeScript
test('check token is set', async ({ page }) => {
const token = await page.evaluate(() => localStorage.getItem('access_token'));
expect(token).toBeTruthy();
expect(token).toMatch(/^eyJ/); // JWT always starts with eyJ
});
