Series Navigation
← Part 6: Network Interception and API Mocking
→ Part 8: Visual Testing — Screenshots and Pixel-Perfect Comparisons
The Problem: Login in Every Test
The most common mistake in growing test suites:
// ❌ Repeated in 50 tests — 50 full login round trips
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
});
If login takes 1 second and you have 100 tests, that's 100 seconds wasted purely on authentication.
The solution: log in once, save the session, reuse it everywhere.
storageState — The Core Concept
storageState saves everything the browser stores after authentication:
- Cookies
localStoragesessionStorage
You capture it once, save it to a file, and tell Playwright to load it at the start of each test — no UI login required.
Step 1: Global Setup — Log In Once
Create tests/global-setup.ts:
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
const browser = await chromium.launch();
const page = await browser.newPage();
// Perform login once
await page.goto(`${baseURL}/login`);
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
// Save the session to a file
await page.context().storageState({ path: '.auth/user.json' });
await browser.close();
}
export default globalSetup;
Create the .auth directory and add it to .gitignore:
mkdir .auth
echo ".auth/" >> .gitignore
Step 2: Register Global Setup in Config
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: './tests/global-setup.ts',
use: {
baseURL: 'http://localhost:3000',
// Load the saved session for every test
storageState: '.auth/user.json',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
Now every test starts already logged in. Zero UI login steps.
Step 3: Override for Tests That Need No Auth
Some tests — like the login page itself — shouldn't start authenticated:
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
// Override storageState for this test file
test.use({ storageState: { cookies: [], origins: [] } });
test('login page is accessible when not authenticated', async ({ page }) => {
await page.goto('/login');
await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
});
test('redirects to login when accessing protected route', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/);
});
Multiple Roles — Admin, User, Guest
Real applications have multiple user roles. Save a session file for each:
// tests/global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function loginAs(baseURL: string, email: string, password: string, savePath: string) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`${baseURL}/login`);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: savePath });
await browser.close();
}
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0].use.baseURL as string;
await Promise.all([
loginAs(baseURL, process.env.ADMIN_EMAIL!, process.env.ADMIN_PASS!, '.auth/admin.json'),
loginAs(baseURL, process.env.USER_EMAIL!, process.env.USER_PASS!, '.auth/user.json'),
]);
}
export default globalSetup;
Role-Based Fixtures
Use fixtures to inject the right user role into each test:
// tests/fixtures/index.ts
import { test as base, Page } from '@playwright/test';
type UserRole = 'admin' | 'user' | 'guest';
type AuthFixtures = {
asAdmin: Page;
asUser: Page;
asGuest: Page;
};
export const test = base.extend<AuthFixtures>({
asAdmin: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: '.auth/admin.json'
});
const page = await context.newPage();
await use(page);
await context.close();
},
asUser: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: '.auth/user.json'
});
const page = await context.newPage();
await use(page);
await context.close();
},
asGuest: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: { cookies: [], origins: [] }
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect } from '@playwright/test';
Use in tests:
import { test, expect } from './fixtures';
test('admin can access user management', async ({ asAdmin }) => {
await asAdmin.goto('/admin/users');
await expect(asAdmin.getByRole('heading', { name: 'User Management' })).toBeVisible();
});
test('regular user cannot access admin panel', async ({ asUser }) => {
await asUser.goto('/admin/users');
await expect(asUser).toHaveURL(/\/403|\/dashboard/);
});
test('guest is redirected to login', async ({ asGuest }) => {
await asGuest.goto('/dashboard');
await expect(asGuest).toHaveURL(/\/login/);
});
Multi-User Scenarios — Two Users Simultaneously
Test real-time features like collaboration or notifications:
test('admin can see when user places an order', async ({ browser }) => {
// Admin session
const adminContext = await browser.newContext({ storageState: '.auth/admin.json' });
const adminPage = await adminContext.newPage();
// User session
const userContext = await browser.newContext({ storageState: '.auth/user.json' });
const userPage = await userContext.newPage();
// Admin opens the orders dashboard
await adminPage.goto('/admin/orders');
// User places an order
await userPage.goto('/shop');
await userPage.getByRole('button', { name: 'Add to Cart' }).first().click();
await userPage.goto('/checkout');
await userPage.getByRole('button', { name: 'Place Order' }).click();
await expect(userPage.getByText('Order confirmed')).toBeVisible();
// Admin's dashboard should show the new order (real-time update)
await expect(adminPage.getByRole('row').first()).toContainText('Pending');
await adminContext.close();
await userContext.close();
});
API-Based Authentication (Faster)
If your app exposes an authentication API, use it instead of UI login — it's 10x faster:
// global-setup.ts
async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
const browser = await chromium.launch();
const context = await browser.newContext();
// Authenticate via API
const response = await context.request.post(`${baseURL}/api/auth/login`, {
data: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD
}
});
const { token } = await response.json();
// Store the token in localStorage
const page = await context.newPage();
await page.goto(baseURL!);
await page.evaluate(t => localStorage.setItem('auth_token', t), token);
await context.storageState({ path: '.auth/user.json' });
await browser.close();
}
Environment Variables for Credentials
Never hardcode credentials. Use a .env file:
# .env.test (add to .gitignore)
TEST_USER_EMAIL=testuser@example.com
TEST_USER_PASSWORD=TestPassword123
ADMIN_EMAIL=admin@example.com
ADMIN_PASS=AdminPassword123
Load in playwright.config.ts:
import dotenv from 'dotenv';
dotenv.config({ path: '.env.test' });
Install dotenv:
npm install dotenv --save-dev
What's Next
In Part 8 we cover visual testing — capturing screenshots, comparing against baselines, testing responsive layouts across viewports, and integrating pixel-diff reporting into your CI pipeline.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.