Playwright has a direct API for cookies through the context object, but localStorage requires page.evaluate(() => localStorage.getItem('key')) because there's no equivalent locator-style method for storage. Both matter for auth testing: storageState captures cookies and localStorage together into a file so every test can start already logged in without touching the login UI. This article covers reading, writing, and clearing cookies, pre-seeding localStorage, testing cookie expiry behavior, and the global setup pattern that generates storageState once and reuses it across the suite.
Reading cookies
import { test, expect } from '@playwright/test';
test('login sets session cookie', async ({ page, context }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page).toHaveURL('/dashboard');
// Read all cookies for the current page
const cookies = await context.cookies();
const sessionCookie = cookies.find(c => c.name === 'session_token');
expect(sessionCookie).toBeDefined();
expect(sessionCookie!.httpOnly).toBe(true); // Can't be read by JS — good
expect(sessionCookie!.secure).toBe(true); // Only sent over HTTPS
expect(sessionCookie!.sameSite).toBe('Strict');
});context.cookies() returns all cookies for all pages in the current context. Filter by name, domain, or path as needed.
Setting cookies
Pre-seed cookies before navigating — useful for testing authenticated states without going through the login flow:
test('authenticated user sees dashboard', async ({ page, context }) => {
// Set auth cookie directly — skip the login UI
await context.addCookies([{
name: 'session_token',
value: 'valid-test-token-abc123',
domain: 'localhost',
path: '/',
httpOnly: true,
secure: false, // false for localhost
sameSite: 'Lax',
}]);
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});storageState instead — it captures cookies AND localStorage in a single file. Manual cookie injection is useful when you need precise control over individual cookie attributes.Clearing cookies
// Clear all cookies for the context
await context.clearCookies();
// Clear a specific cookie (by navigating to a clear endpoint or using CDP)
// Most apps provide a /logout endpoint that clears the session cookie server-side
await page.goto('/logout');Testing cookie expiry behavior
test('expired session redirects to login', async ({ page, context }) => {
// Set an expired cookie
const yesterday = new Date(Date.now() - 86400000);
await context.addCookies([{
name: 'session_token',
value: 'expired-token',
domain: 'localhost',
path: '/',
expires: yesterday.getTime() / 1000, // Unix timestamp in seconds
}]);
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});Working with localStorage
Playwright doesn't have a direct localStorage API — you access it through page.evaluate():
// Read localStorage
const theme = await page.evaluate(() => localStorage.getItem('theme'));
expect(theme).toBe('dark');
// Set localStorage before page interaction
await page.evaluate(() => {
localStorage.setItem('theme', 'dark');
localStorage.setItem('user_preferences', JSON.stringify({ fontSize: 'large' }));
});
// Clear specific key
await page.evaluate(() => localStorage.removeItem('theme'));
// Clear all localStorage
await page.evaluate(() => localStorage.clear());Testing that localStorage persists preferences
test('dark mode preference persists after refresh', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('switch', { name: 'Dark mode' }).click();
// Verify it's stored
const stored = await page.evaluate(() => localStorage.getItem('theme'));
expect(stored).toBe('dark');
// Reload the page
await page.reload();
// Verify the preference was restored from localStorage
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
});Pre-seeding localStorage
test('user with large font preference sees larger text', async ({ page }) => {
await page.goto('/'); // Navigate first to set the domain
await page.evaluate(() => {
localStorage.setItem('fontSize', 'large');
});
await page.reload(); // Reload so the app reads the preference
const body = await page.locator('body');
await expect(body).toHaveCSS('font-size', '18px');
});sessionStorage
Same API as localStorage, different scope — sessionStorage clears when the tab closes:
// Read sessionStorage
const cartItems = await page.evaluate(() => sessionStorage.getItem('cart'));
// Set sessionStorage
await page.evaluate(() => {
sessionStorage.setItem('cart', JSON.stringify([{ id: 1, qty: 2 }]));
});Saving and restoring full browser state
For auth testing at scale, capture the entire storage state (cookies + localStorage) into a file:
// global-setup.ts — run once before all tests
import { chromium } from '@playwright/test';
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/login');
await page.getByLabel('Email').fill('testuser@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
// Save cookies + localStorage to file
await page.context().storageState({ path: 'playwright/.auth/user.json' });
await browser.close();
}
export default globalSetup;Then reuse in tests without logging in:
// playwright.config.ts
projects: [{
name: 'authenticated',
use: { storageState: 'playwright/.auth/user.json' },
}],This is the recommended pattern for authenticated test suites — faster, more reliable, and doesn't depend on the login UI working.
→ See also: Handling Auth in Playwright with storageState (No Logging In Every Test) | Test Isolation: Why Each Playwright Test Should Be Stateless | Global Setup and Teardown in Playwright