Introduction
Every serious Playwright test suite needs setup and teardown logic — logging in a user, seeding a database, resetting state, or capturing artifacts on failure. Playwright gives you four hooks to control this lifecycle: beforeAll, beforeEach, afterEach, and afterAll.
Used correctly, hooks make your tests faster, cleaner, and more reliable. Used incorrectly, they cause hidden coupling, flaky failures, and slow suites. This guide covers every hook with real examples, execution order, scoping rules, and when to reach for a fixture instead.
What Are Playwright Hooks?
Hooks are special functions that run automatically at defined points around your tests. They come from @playwright/test and let you inject setup or cleanup code without repeating it in every test.
- beforeAll — runs once before all tests in a file or describe block.
- beforeEach — runs before every individual test.
- afterEach — runs after every individual test.
- afterAll — runs once after all tests in a file or describe block.
The Four Hook Types
import { test, expect } from '@playwright/test';
test.beforeAll(async () => {
console.log('Runs ONCE before all tests');
});
test.beforeEach(async ({ page }) => {
console.log('Runs before EACH test');
await page.goto('https://example.com');
});
test.afterEach(async ({ page }, testInfo) => {
console.log(`Runs after EACH test: ${testInfo.title} — ${testInfo.status}`);
});
test.afterAll(async () => {
console.log('Runs ONCE after all tests');
});
test('first test', async ({ page }) => {
await expect(page).toHaveTitle(/Example/);
});
test('second test', async ({ page }) => {
await expect(page.locator('h1')).toBeVisible();
});Execution Order
Playwright guarantees a strict order. For a file with two tests, the sequence is:
- beforeAll
- beforeEach → test 1 → afterEach
- beforeEach → test 2 → afterEach
- afterAll
beforeAll in Practice
Use beforeAll for expensive, read-only setup that every test in the file shares — seeding data, generating tokens, or warming up an API client.
import { test, expect, request, APIRequestContext } from '@playwright/test';
let apiContext: APIRequestContext;
let authToken: string;
test.beforeAll(async () => {
apiContext = await request.newContext({ baseURL: 'https://api.example.com' });
const res = await apiContext.post('/login', {
data: { email: 'qa@example.com', password: 'secret' },
});
authToken = (await res.json()).token;
});
test('fetch user profile', async () => {
const res = await apiContext.get('/me', {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(res.status()).toBe(200);
});
test.afterAll(async () => {
await apiContext.dispose();
});beforeEach in Practice
Use beforeEach for setup that must be fresh for each test — navigating to a starting URL, resetting UI state, or logging in through a persisted storage state.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('shows welcome message', async ({ page }) => {
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('shows sidebar links', async ({ page }) => {
await expect(page.getByRole('link', { name: 'Reports' })).toBeVisible();
});afterEach in Practice
afterEach is perfect for per-test cleanup — clearing cookies, deleting created records, or capturing artifacts when a test fails.
import { test } from '@playwright/test';
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
const screenshotPath = testInfo.outputPath(`failure-${testInfo.title}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
await testInfo.attach('failure-screenshot', {
path: screenshotPath,
contentType: 'image/png',
});
}
});afterAll in Practice
Use afterAll to release resources created in beforeAll — disposing API contexts, closing DB connections, or deleting seed data.
import { test } from '@playwright/test';
import { seedUsers, deleteUsers } from '../utils/db';
let createdIds: string[] = [];
test.beforeAll(async () => {
createdIds = await seedUsers(5);
});
test.afterAll(async () => {
await deleteUsers(createdIds);
});Scoping with describe Blocks
Hooks respect the block they're declared in. A hook inside a describe block only runs for tests within that block — top-level hooks apply to the whole file.
import { test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('admin section', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin');
});
test('shows admin panel', async ({ page }) => {
// Runs: top beforeEach -> describe beforeEach -> test
});
});
test('home page loads', async ({ page }) => {
// Runs: top beforeEach -> test (describe beforeEach is skipped)
});Hooks vs Fixtures
Hooks are convenient but global to a file — they share state through module-level variables and can't be reused across files. Fixtures solve the same problem in a composable, type-safe way.
// Hook approach - state in module scope
let token: string;
test.beforeAll(async () => { token = await login(); });
// Fixture approach - reusable across files
export const test = base.extend<{ token: string }>({
token: async ({}, use) => {
const t = await login();
await use(t);
},
});- Use hooks for per-file setup that won't be reused elsewhere.
- Use fixtures when the same setup is needed across multiple spec files.
- Fixtures give you dependency injection, automatic cleanup, and per-test isolation without shared module state.
Best Practices
- Keep hooks small — they run for every test, so slow hooks slow the whole suite.
- Never assert business logic inside a hook. Assertions belong in tests so failures point at the right place.
- Prefer beforeEach over beforeAll for anything that mutates state — shared mutable state causes flaky, order-dependent tests.
- Always pair beforeAll with afterAll for resources like API contexts, DB connections, or spawned processes.
- Use testInfo.status inside afterEach to capture artifacts only on failure — avoids screenshot noise on green runs.
- Move reusable setup into a fixture as soon as more than one spec file needs it.
Common Mistakes
- Sharing mutable state in beforeAll and relying on test execution order — Playwright can parallelise tests and reorder them.
- Doing heavy UI login in beforeEach for every test instead of using storageState — kills suite speed.
- Forgetting that beforeAll runs per worker, not globally. Use a globalSetup file for truly one-time setup.
- Throwing inside afterAll — errors there mark the whole file as failed even if every test passed.
- Mixing hooks and fixtures for the same concern, which duplicates work and hides ownership.
Summary
Hooks are the backbone of Playwright test lifecycle management. beforeAll and afterAll handle expensive one-time setup and cleanup per worker. beforeEach and afterEach keep every test isolated and capture artifacts on failure.
As your framework grows, promote repeated hook logic into fixtures — you get reusability, type safety, and automatic cleanup for free. Combined with globalSetup for one-time initialization, hooks and fixtures let you scale from a handful of tests to enterprise-grade suites without losing reliability.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.
Playwright Framework Series
View all →- 1Getting Started with Playwright: Installation, Setup, and Your First Test
- 2Playwright Locators: The Complete Guide with Real-World Examples
- 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
- 4Playwright Assertions: The Complete Guide with Real-World Examples
- 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
- 6Playwright Fixtures: The Complete Guide with Real-World Examples
- 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
- 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
- 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
- 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
- 11Page Object Model with Playwright: A Practical Guide
- 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
- 13Building an Enterprise Playwright Framework from Scratch
API Testing Series
View all →- 1Playwright API Testing: The Complete Guide with Real-World Examples
- 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
- 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
- 4Part 3: Mastering CRUD Operations in Playwright API Testing
- 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
- 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
- 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
- 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
- 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
- 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
- 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
- 12API Testing with Playwright: Request Context, Auth, and Assertions