Playwright Hooks: beforeEach, afterEach, beforeAll and afterAll Explained

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.

Advertisement

📖 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:

HookRunsTypical use
test.beforeEachBefore every test in its scopeOpen the page, log in
test.afterEachAfter every test in its scopeLog results, clean up data, extra screenshots
test.beforeAllOnce, before all tests in its scope (per worker)Create test data via API, start a service
test.afterAllOnce, 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:

  1. File-level beforeAll (once per worker)
  2. Describe-level beforeAll (once per worker)
  3. File-level beforeEach
  4. Describe-level beforeEach
  5. The test
  6. Describe-level afterEach
  7. File-level afterEach
  8. Describe-level afterAll (after the last test in the block)
  9. 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:

HooksFixtures
Reuse across filesCopy or import per fileDefined once, used anywhere
Setup and teardown togetherSplit into before and afterIn one function around use()
RunsFor every test in scopeOnly for tests that request it
Passes data to testsThrough shared variablesDirectly as a parameter
Best forSimple, file-specific setupReusable 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 beforeAll variables 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 afterEach cleanup for data your test creates, so reruns start fresh.
  • Prefer a setup project with storageState over logging in through the UI in every beforeEach for large suites (save login session).

📝 Assignment

  1. Create tests/hooks.spec.ts with a beforeEach that logs in to Sauce Demo, and three tests: add to cart, sort by price, open the cart.
  2. Add an afterEach that prints the test title and status, and attaches a full-page screenshot only when a test fails.
  3. Add beforeAll and afterAll hooks that print a message; run with --workers=1 and then --workers=3 and explain the difference in output.
  4. Wrap two tests in a test.describe with their own beforeEach, and write down the execution order you observe.
  5. Use beforeAll with the request fixture to create a post on jsonplaceholder.typicode.com and assert its id in a test.
  6. Bonus: try to use page inside beforeAll, read the error, then fix it with browser.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

🎬 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”

Leave a Comment