Hooks let you run setup and cleanup code automatically around your tests, so every test starts from a known state without repeating code. This lesson explains all four Playwright hooks, their scope and order, common pitfalls, and when fixtures are the better choice.
📖 What You’ll Learn: beforeEach and afterEach, acting on failures with testInfo, beforeAll and afterAll with worker fixtures, hook scope inside describe blocks, execution order, titled hooks, hooks vs fixtures, and best practices.
🪝 What Are Hooks?
Hooks are functions that run automatically before or after your tests. They remove repetition: instead of writing the same login steps at the start of ten tests, you write them once in a hook.
Playwright Test has four hooks:
| Hook | Runs | Typical use |
|---|---|---|
test.beforeEach | Before every test in its scope | Open the page, log in |
test.afterEach | After every test in its scope | Log results, clean up data, extra screenshots |
test.beforeAll | Once, before all tests in its scope (per worker) | Create test data via API, start a service |
test.afterAll | Once, after all tests in its scope (per worker) | Delete test data, close connections |
🔁 beforeEach and afterEach
The most common hook. Here every test starts on the logged-in products page:
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.locator('.title')).toHaveText('Products');
});
test.afterEach(async ({ page }, testInfo) => {
console.log(testInfo.title + ' finished with status: ' + testInfo.status);
});
test('add backpack to cart', async ({ page }) => {
await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});
test('sort products by price', async ({ page }) => {
await page.locator('[data-test="product-sort-container"]').selectOption('lohi');
await expect(page.locator('.inventory_item_price').first()).toHaveText('$7.99');
});
Hooks receive the same fixtures as tests (page, context, request…). The second argument, testInfo, describes the current test: its title, status, retry number and more.
Do something only when a test fails
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
const image = await page.screenshot({ fullPage: true });
await testInfo.attach('failure-full-page', { body: image, contentType: 'image/png' });
console.log('Failed on URL: ' + page.url());
}
});
🧱 beforeAll and afterAll
beforeAll runs once before the tests in its scope. It is ideal for expensive setup that tests can share, such as creating data through an API:
import { test, expect } from '@playwright/test';
let postId: number;
test.beforeAll(async ({ request }) => {
const response = await request.post('https://jsonplaceholder.typicode.com/posts', {
data: { title: 'Playwright hooks', body: 'demo', userId: 1 },
});
expect(response.ok()).toBeTruthy();
postId = (await response.json()).id;
console.log('Created post ' + postId);
});
test.afterAll(async ({ request }) => {
await request.delete('https://jsonplaceholder.typicode.com/posts/' + postId);
console.log('Deleted post ' + postId);
});
test('post id was created', async () => {
expect(postId).toBeGreaterThan(0);
});
⚠️ Common mistake: beforeAll and afterAll can only use worker-scoped fixtures such as browser and request. The page fixture belongs to a single test and is not available in beforeAll. If you need a page there, create one: const page = await browser.newPage(), and close it in afterAll.
📝 Note: “Once” means once per worker process. With four parallel workers, a top-level beforeAll in a file can run up to four times, and it runs again after a failed test, because Playwright restarts the worker. For setup that must run exactly once for the whole run, use a setup project or globalSetup (config file).
📦 Hook Scope with test.describe
Hooks declared at the top of a file apply to every test in the file. Hooks inside a test.describe block apply only to the tests in that block:
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://www.saucedemo.com/'); // runs for ALL tests in the file
});
test.describe('valid login', () => {
test.beforeEach(async ({ page }) => { // runs only in this block
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
});
test('shows products', async ({ page }) => {
await expect(page.locator('.title')).toHaveText('Products');
});
});
test.describe('invalid login', () => {
test('locked out user sees an error', async ({ page }) => {
await page.getByPlaceholder('Username').fill('locked_out_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.locator('[data-test="error"]')).toContainText('locked out');
});
});
🔢 Execution Order
For a test inside a describe block, Playwright runs:
- File-level
beforeAll(once per worker) - Describe-level
beforeAll(once per worker) - File-level
beforeEach - Describe-level
beforeEach - The test
- Describe-level
afterEach - File-level
afterEach - Describe-level
afterAll(after the last test in the block) - File-level
afterAll(after the last test in the file)
Before hooks run from the outside in; after hooks run from the inside out.
// Hooks can have titles, which appear in the report and traces
test.beforeEach('open the login page', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
});
⚖️ Hooks vs Fixtures
Both remove repetition, but they work differently:
| Hooks | Fixtures | |
|---|---|---|
| Reuse across files | Copy or import per file | Defined once, used anywhere |
| Setup and teardown together | Split into before and after | In one function around use() |
| Runs | For every test in scope | Only for tests that request it |
| Passes data to tests | Through shared variables | Directly as a parameter |
| Best for | Simple, file-specific setup | Reusable setup: logged-in pages, page objects, test data |
Start with hooks while learning. As your suite grows, move shared setup into custom fixtures.
✅ Best Practices
- Keep hooks short; tests should still be readable without scrolling up.
- Do not share mutable state between tests through
beforeAllvariables unless tests run in serial mode; each test should work alone. - Put assertions in hooks only to confirm setup succeeded (for example, login reached the products page).
- Use
afterEachcleanup for data your test creates, so reruns start fresh. - Prefer a setup project with storageState over logging in through the UI in every
beforeEachfor large suites (save login session).
📝 Assignment
- Create
tests/hooks.spec.tswith abeforeEachthat logs in to Sauce Demo, and three tests: add to cart, sort by price, open the cart. - Add an
afterEachthat prints the test title and status, and attaches a full-page screenshot only when a test fails. - Add
beforeAllandafterAllhooks that print a message; run with--workers=1and then--workers=3and explain the difference in output. - Wrap two tests in a
test.describewith their ownbeforeEach, and write down the execution order you observe. - Use
beforeAllwith therequestfixture to create a post on jsonplaceholder.typicode.com and assert its id in a test. - Bonus: try to use
pageinsidebeforeAll, read the error, then fix it withbrowser.newPage().
❓ FAQ
🤔 What is the difference between beforeEach and beforeAll in Playwright?
beforeEach runs before every test and can use the page fixture. beforeAll runs once per worker before all tests in its scope and can only use worker-scoped fixtures such as browser and request.
🤔 Why can’t I use page in beforeAll?
The page fixture is created fresh for each test, so it does not exist yet in beforeAll. Create a page manually with browser.newPage() if you need one.
🤔 Why does my beforeAll run more than once?
beforeAll runs once per worker. With several workers, or after a test failure that restarts the worker, it runs again. Use globalSetup or a setup project for one-time setup.
🤔 Should I use hooks or fixtures in Playwright?
Hooks are fine for simple setup within a file. Fixtures are better for reusable setup across files, because they combine setup and teardown and only run for tests that use them.
🤔 How do I know if a test failed inside afterEach?
Compare testInfo.status with testInfo.expectedStatus; if they differ, the test failed unexpectedly.
🎯 Next Steps
- 🎭 Group, Tag and Filter Tests
- 🎭 Annotations: skip, only, fixme, slow
- 🎭 Playwright Fixtures Step by Step
- 🎭 Playwright Tutorial with TypeScript
🎬 Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
1 thought on “Playwright Hooks: beforeEach, afterEach, beforeAll and afterAll Explained”