How to Use `beforeEach` in Playwright Tests
Learn how Playwright's beforeEach hook works, where it runs, how to use fixtures, and when a fixture is better than a hook.
test.beforeEach runs setup before every Playwright test in the current file or enclosing test.describe group. Register an async callback, request the fixtures you need (such as page), and await each asynchronous action.
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/);
});
The hook above navigates before every test in the file. Use a beforeEach hook for small, local setup. Use a custom fixture when setup must be reused across files, composed with other setup, created only when requested, or paired with teardown.
What beforeEach does
Playwright Test executes a registered beforeEach callback immediately before each applicable test. The callback can request the same fixtures as a test body, and it can receive an optional TestInfo object.
- A file-level hook applies to every test in that file.
- A hook inside
test.describeapplies only to tests in that group. - Multiple applicable hooks run in registration order.
- If one hook fails, Playwright continues running other applicable hooks before reporting the test setup failure.
- An optional title makes the hook easier to identify in reports.
The official API reference documents the hook signature and scope in Playwright’s test API documentation.
Basic setup patterns
Navigate before every test
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/account');
});
test('shows the account heading', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
test('shows the account menu', async ({ page }) => {
await expect(page.getByRole('navigation')).toBeVisible();
});
Add a descriptive hook title
test.beforeEach('Open account page', async ({ page }) => {
await page.goto('https://example.com/account');
});
Limit setup to a test.describe group
import { test, expect } from '@playwright/test';
test('public landing page works without account setup', async ({ page }) => {
await page.goto('https://example.com/');
await expect(page).toHaveTitle(/Example/);
});
test.describe('signed-in account', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.goto('https://example.com/account');
});
test('shows profile details', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();
});
});
Using fixtures in beforeEach
Fixtures are supplied through the callback’s destructured argument. The built-in page fixture gives each test an isolated browser page. Other commonly used fixtures include context, browser, request, and testInfo.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page, context }, testInfo) => {
await context.setExtraHTTPHeaders({
'x-test-run': testInfo.testId
});
await page.goto('https://example.com/');
});
test('renders the home page', async ({ page }) => {
await expect(page.locator('body')).toBeVisible();
});
Playwright creates a fresh test context for each test while the browser instance is shared by tests in the same worker. Requesting page in both the hook and test lets Playwright manage that page’s lifetime automatically. See the official fixtures guide for fixture setup and teardown behavior.
Hook scope and ordering
File-level and group-level hooks
import { test } from '@playwright/test';
test.beforeEach(async () => {
console.log('file setup');
});
test.describe('checkout', () => {
test.beforeEach(async () => {
console.log('checkout setup');
});
test('can pay', async () => {
// Both hooks run, in registration order.
});
});
Keep broad setup at file scope only when every test needs it. Put narrower setup inside the smallest matching describe group. This reduces unnecessary navigation and makes a test’s prerequisites visible near the tests that use them.
Several hooks
test.beforeEach('Create test data', async ({ request }) => {
await request.post('/api/test-data', { data: { status: 'ready' } });
});
test.beforeEach('Open dashboard', async ({ page }) => {
await page.goto('/dashboard');
});
Applicable hooks run in registration order. If setup has dependencies, register the prerequisite first or combine the dependent actions in one clearly named hook.
beforeEach versus beforeAll
| Need | Use | Runs |
|---|---|---|
| Fresh setup for every test | test.beforeEach |
Before each test |
| One-time setup for a file or group | test.beforeAll |
Once per worker process |
| Reusable setup with teardown | Custom fixture | According to fixture scope and use |
Do not use beforeAll merely to avoid repeating setup when tests need isolation. Shared mutable state can make tests order-dependent. Use it for genuinely shared, worker-scoped resources.
When a fixture is better
A hook is a good fit for a short action such as opening a URL. A fixture is usually clearer when the setup is reused, needs teardown, should be lazy, or must compose with other fixtures.
import { test as base, expect } from '@playwright/test';
type Fixtures = {
accountPage: void;
};
export const test = base.extend<Fixtures>({
accountPage: [async ({ page }, use) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.goto('https://example.com/account');
await use();
// Teardown belongs next to setup when cleanup is required.
await page.context().clearCookies();
}, { auto: false }]
});
export { expect };
// account.spec.ts
import { test, expect } from './fixtures';
test('shows the account heading', async ({ accountPage, page }) => {
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
For setup that should run automatically before every test without repeating a hook declaration, define an automatic test-scoped fixture. The fixtures guide explains this pattern and its teardown semantics.
Timeouts and slow setup
Time spent in beforeEach counts toward the test timeout. A slow login or navigation can therefore fail the test before its assertions run. Prefer reliable locators and targeted setup first. If a known slow hook needs more time, increase the timeout deliberately:
import { test } from '@playwright/test';
test.beforeEach(async ({ page }, testInfo) => {
test.setTimeout(testInfo.timeout + 30_000);
await page.goto('https://example.com/slow-start');
});
Check the timeout configured in your Playwright project before changing it. Increasing timeouts globally can hide regressions and lengthen failed runs.
Common mistakes and fixes
| Symptom | Cause | Fix |
|---|---|---|
| Setup runs for unrelated tests | The hook is registered at file scope. | Move it inside the relevant test.describe. |
| The test starts before navigation or login completes | An async action is missing await. |
Await every navigation, fill, click, API call, and wait. |
| Tests affect one another | Mutable data or storage is shared. | Use test fixtures and isolated contexts; reset data per test. |
| Hook times out | Setup exceeds the test timeout or waits for an unavailable condition. | Inspect the failing step, use a precise readiness assertion, and adjust timeout only when justified. |
| Login code is duplicated across files | Setup belongs in a reusable fixture. | Move authentication into a custom fixture or shared setup project. |
| Cleanup is missing | A resource was created in a hook without a paired teardown. | Use a fixture with await use() and cleanup afterward. |
| Hook order is surprising | Several file and group hooks apply. | Review registration order and keep dependent setup together. |
Reliable and fast beforeEach setup
- Navigate directly to the page under test instead of clicking through a long UI path.
- Wait for a meaningful UI condition, such as a heading or authenticated control, rather than an arbitrary delay.
- Keep each test independent so retries do not depend on another test’s state.
- Use API requests or fixtures to create data when the UI flow itself is not under test.
- Do not put expensive one-time work in
beforeEach; usebeforeAllor a worker-scoped fixture when sharing is safe. - Record traces and screenshots on failure through your Playwright configuration so hook failures are diagnosable.
Or skip the browser setup
If your goal is to capture a page for a test artifact, visual review, or documentation, ScreenshotNeo makes a screenshot with one request. Its API accepts the URL and returns PNG, JPEG, WebP, or PDF; the API documentation lists all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://playwright.dev -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://playwright.dev"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://playwright.dev' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no cost.
FAQ
Does beforeEach run once per worker?
No. It runs before each applicable test. The browser may be shared by tests in a worker, but test-scoped contexts and pages are isolated.
Can a hook use page?
Yes. Destructure page from the callback argument and await its actions.
Should login go in beforeEach?
Use it when each test needs a fresh authenticated state and login is part of the behavior you want to exercise. For faster, reusable authentication, use a fixture or stored authentication state.
When should I use beforeAll?
Use it for setup that is intentionally shared once per file or group. Keep per-test state in beforeEach or a test-scoped fixture.
Can I increase only the hook timeout?
Playwright’s timeout applies to the test, including its hooks. You can adjust the test timeout from the hook with test.setTimeout, using the current TestInfo timeout as a baseline.


