Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

test.beforeEach runs setup before every Playwright test in the file where it is registered, or before every test inside the enclosing test.describe group. Import test from @playwright/test, register an asynchronous callback, and request fixtures such as page in that callback’s first argument.

Use a hook for simple, local preparation such as opening a URL or signing in. Move setup into a fixture when it must be reused across files, composed with other setup, created only when requested, or paired with teardown.

Basic beforeEach syntax

Install Playwright Test in your project, then import test and expect from @playwright/test. The callback is asynchronous because browser operations such as navigation return promises.

import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('https://playwright.dev/');
});

test('shows the Playwright title', async ({ page }) => {
  await expect(page).toHaveTitle(/Playwright/);
});

Playwright creates the test-scoped page fixture for the hook and for the test. The await on page.goto ensures navigation completes before the test starts. Every test in this file performs that navigation first.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Where the hook applies

File-level setup

A hook registered directly in the test module applies to every test in that file, including tests declared later in the file.

import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('https://example.com/dashboard');
});

test('shows the dashboard heading', async ({ page }) => {
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

test('shows the account menu', async ({ page }) => {
  await expect(page.getByRole('button', { name: 'Account' })).toBeVisible();
});

Group-specific setup with test.describe

Put the registration inside a test.describe callback when only that group needs the setup. Tests outside the group do not run this hook.

import { test, expect } from '@playwright/test';

test('public landing page is reachable', async ({ page }) => {
  await page.goto('https://example.com/');
  await expect(page).toHaveTitle(/Example/);
});

test.describe('signed-in dashboard', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL ?? 'user@example.com');
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD ?? 'password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  });

  test('shows recent activity', async ({ page }) => {
    await expect(page.getByRole('region', { name: 'Recent activity' })).toBeVisible();
  });
});

Keep credentials in environment variables or Playwright’s authentication-state configuration rather than committing real passwords. The example fallback values are placeholders for local demonstration only.

Fixtures you can request in a hook

The callback argument uses the same fixture mechanism as a test. Request only what setup needs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • page gives the test an isolated browser page.
  • Other built-in or custom fixtures can be destructured in the same way.
  • The callback may also accept a second TestInfo argument for metadata such as the current timeout.
import { test } from '@playwright/test';

test.beforeEach(async ({ page }, testInfo) => {
  await page.goto('https://example.com/');
  console.log(`Preparing ${testInfo.title}`);
});

The browser instance is shared across tests in a worker, but each test receives an isolated browser context. Playwright’s fixture lifecycle creates and tears down test-scoped resources around each test, so a hook normally should not manually construct a browser or context.

Multiple hooks and execution order

When more than one applicable beforeEach hook exists, Playwright runs them in registration order. A file-level hook and a hook inside a matching test.describe both apply to tests in that group.

test.beforeEach(async ({ page }) => {
  await page.goto('https://example.com/');
});

test.describe('checkout', () => {
  test.beforeEach(async ({ page }) => {
    await page.getByRole('link', { name: 'Shop' }).click();
  });

  test('can open the cart', async ({ page }) => {
    // The file hook runs first, then the checkout hook.
  });
});

Playwright continues running applicable hooks even if one fails. The failure is reported, and later hook execution can expose additional errors. Do not rely on a failed hook having prepared state for subsequent hooks; make dependencies explicit or combine tightly coupled setup into one operation.

You can give a hook a title. This makes reports and error messages easier to interpret:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
test.beforeEach('Open start URL', async ({ page }) => {
  await page.goto('https://example.com/');
});

beforeEach versus beforeAll

beforeEach runs for every test. Use beforeAll when setup should run once before all tests in a file or group (once per worker process). Choosing the wrong hook can cause either unnecessary work or shared mutable state.

Hook Runs Typical use State implication
beforeEach Before every applicable test Navigate, authenticate, seed isolated test data Fresh test-scoped fixtures are available
beforeAll Once before the file or group in a worker Expensive worker-level preparation Do not assume a page or context can be safely shared between tests

If a test mutates data, per-test setup is usually safer than sharing a page or account prepared once for an entire group.

When a fixture is a better design

A hook is concise, but a fixture expresses a resource’s lifetime and can be reused by many files. Prefer a fixture when one or more of these conditions apply:

  • The setup is needed in multiple test files.
  • Several setup components should compose without copying hook code.
  • The resource should be created only when a test requests it.
  • Setup has paired teardown that belongs next to its creation.
  • You need a consistent global per-test behavior.

Reusable fixture with teardown

import { test as base } from '@playwright/test';

type Fixtures = {
  signedInPage: void;
};

export const test = base.extend<Fixtures>({
  signedInPage: [async ({ page }, use) => {
    await page.goto('https://example.com/login');
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL ?? 'user@example.com');
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD ?? 'password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await use();
    // Put cleanup here when the setup creates data requiring removal.
  }, { auto: false }]
});

export { expect } from '@playwright/test';

Tests import this extended test object and request signedInPage when needed. A fixture can also be automatic and test-scoped, which supplies global per-test setup without repeating a hook declaration in every file.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Automatic per-test fixture

export const test = base.extend<{ appReady: void }>({
  appReady: [async ({ page }, use) => {
    await page.goto('https://example.com/');
    await use();
  }, { auto: true }]
});

Use automatic fixtures deliberately: every test that imports this custom test object pays the setup cost, even when the test does not otherwise need the resource.

Timeouts and slow setup

Time spent in beforeEach counts toward the test timeout shared with the test body. A slow login, data seed, or navigation can therefore make an otherwise short assertion time out.

test.beforeEach(async ({ page }, testInfo) => {
  test.setTimeout(testInfo.timeout + 30_000);
  await page.goto('https://example.com/slow-start');
});

Check the timeout configured in the installed Playwright version and your project configuration before increasing it. A larger timeout can mask a regression; first remove unnecessary navigation, wait for a specific readiness signal instead of an arbitrary delay, and avoid repeating expensive setup when a fixture or authentication state can provide it more efficiently.

Reliable setup patterns

Wait for an observable condition

Navigation alone does not prove that an application is ready. After goto, wait for a role, label, or other stable locator that represents readiness.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
test.beforeEach(async ({ page }) => {
  await page.goto('https://example.com/app');
  await expect(page.getByRole('heading', { name: 'Workspace' })).toBeVisible();
});

Keep hooks focused

A hook should establish a predictable starting state. Assertions about the behavior under test belong in the test body unless the assertion is specifically a readiness check. Long hooks make failures harder to diagnose because every test fails before reaching its own assertions.

Make test data independent

If tests edit the same records, one test can affect another even though each receives a new context. Generate unique data or reset the relevant state during setup and clean it up in a fixture or dedicated teardown mechanism.

Common mistakes and fixes

Symptom Cause Fix
The test starts before navigation finishes await was omitted Await asynchronous operations such as page.goto, clicks that trigger navigation, and API calls.
Setup runs for unrelated tests The hook was registered at file scope Move it inside the relevant test.describe.
A test times out before its assertion The hook consumed the shared timeout Profile the setup, wait for a precise readiness condition, or adjust the timeout intentionally.
State leaks between tests Mutable data or browser objects are shared manually Use test-scoped fixtures, isolated data, and Playwright’s context isolation.
Cleanup is scattered or skipped A resource is created in beforeEach and removed in a separate hook Define a fixture with setup and teardown in one place.
A hook appears not to run The test imports a different test object or is outside the hook’s scope Check imports, nesting, and the project’s test file selection.
Login works locally but fails in CI Credentials, base URL, or environment variables differ Validate CI secrets and configuration, and report a clear readiness locator after login.

Debugging a failing beforeEach

  1. Read the hook title and first failing action in the report.
  2. Run the affected test alone to remove noise from other tests.
  3. Log the current test title through testInfo and inspect the page at the failure point.
  4. Replace broad sleeps with locator-based expectations.
  5. Verify that the hook is registered at the intended file or describe scope.
  6. Check whether a fixture would make the setup lifecycle clearer.

If a hook fails, the test itself may not execute. Treat the hook as production-quality test code: use stable locators, explicit awaits, deterministic data, and useful titles.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is to capture a page rather than run an interactive Playwright test, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF output. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for all options, including full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets, custom viewport and retina scale, PDF paper and margin controls, custom CSS or JavaScript, clicks before capture, selector and network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I use beforeEach inside nested test.describe blocks?

Yes. Each test runs the applicable hooks from its enclosing scopes in registration order, so keep nested setup specific and predictable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can a beforeEach hook access the test name?

Yes. Add the optional TestInfo argument and read its metadata, including the current title and timeout.

Should authentication run in beforeEach every time?

For simple local suites it can. For larger suites, a reusable authentication fixture or stored authentication state can reduce duplication and setup time while preserving test isolation.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.