---
title: Fixtures, Hooks and Logging In Once | TestingBot
description: 'Every test so far has asked for `{ page }`. That object is a **fixture**:
  a value the runner sets up before the test, hands to it, and tears down afterwards.…'
source_url:
  html: https://testingbot.com/resources/courses/playwright/fixtures-hooks-and-authentication
  md: https://testingbot.com/resources/courses/playwright/fixtures-hooks-and-authentication.md
---

Course

[Playwright Tutorial: Zero to CI](https://testingbot.com/resources/courses/playwright)

Chapter 5 of 10

1. [1 Playwright Tutorial for Beginners](https://testingbot.com/resources/courses/playwright)
2. [2 Install Playwright and Run a First Test](https://testingbot.com/resources/courses/playwright/install-and-first-test)
3. [3 Locators: Find Elements the Way Users Do](https://testingbot.com/resources/courses/playwright/locators)
4. [4 Auto-Waiting and Web-First Assertions](https://testingbot.com/resources/courses/playwright/auto-waiting-and-assertions)
5. [5 Fixtures, Hooks and Logging In Once](https://testingbot.com/resources/courses/playwright/fixtures-hooks-and-authentication)
6. [6 Debugging: UI Mode, Trace Viewer, Codegen](https://testingbot.com/resources/courses/playwright/debugging-tests)
7. [7 Multiple Browsers, Workers and Sharding](https://testingbot.com/resources/courses/playwright/browsers-parallelism-and-sharding)
8. [8 Run Playwright in CI with GitHub Actions](https://testingbot.com/resources/courses/playwright/github-actions-ci)
9. [9 Run Your Playwright Suite on a Cloud Grid](https://testingbot.com/resources/courses/playwright/run-on-a-cloud-grid)
10. [10 Knowledge check](https://testingbot.com/resources/courses/playwright/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 5 of 10 Beginner 8 min read 3 quick checks Updated Nov 2026 

# Fixtures, Hooks and Logging In Once
On this page
1. [The built-in fixtures](https://testingbot.com#the-built-in-fixtures)
2. [Hooks](https://testingbot.com#hooks)
3. [Writing your own fixture](https://testingbot.com#writing-your-own-fixture)
4. [Logging in once with a setup project](https://testingbot.com#logging-in-once-with-a-setup-project)

Every test so far has asked for `{ page }`. That object is a **fixture** : a value the runner sets up before the test, hands to it, and tears down afterwards. Fixtures are how Playwright Test shares setup between tests without shared mutable state, and they are also the cleanest way to solve the problem every real suite hits early: logging in.

## The built-in fixtures[#](https://testingbot.com#the-built-in-fixtures)

| Fixture | Scope | What you get |
| --- | --- | --- |
| `page` | test | a new page in a new browser context |
| `context` | test | the browser context that `page` belongs to |
| `browser` | worker | the browser process, shared by tests in a worker |
| `browserName` | worker | `'chromium'`, `'firefox'` or `'webkit'` |
| `request` | test | an API client for HTTP calls without a browser |

Fixtures are created lazily. A test that only asks for `request` never launches a browser.

## Hooks[#](https://testingbot.com#hooks)

Hooks are familiar from other runners and still useful for simple, file-local setup:

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

test.describe('todo filters', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://demo.playwright.dev/todomvc/');
  });

  test.afterEach(async ({ page }, testInfo) => {
    if (testInfo.status !== testInfo.expectedStatus) {
      console.log(`${testInfo.title} failed on ${page.url()}`);
    }
  });

  test('starts empty', async ({ page }) => {
    await expect(page.getByTestId('todo-item')).toHaveCount(0);
  });
});
```

`beforeAll` and `afterAll` also exist. Note that they run once per **worker** , not once per run. With four workers, a `beforeAll` that seeds a database row runs up to four times. Keep expensive one-time setup out of `beforeAll`; the setup projects later in this chapter are the right tool.

A `beforeAll` inserts one seed row into a shared database. With four workers you find four copies of the row. Why?

 `fullyParallel` runs `beforeAll` before every test `beforeAll` runs once per worker, not once per run Retries re-ran the hook three extra times

Each worker is a separate process that runs the file's hooks itself, so the hook runs up to once per worker. Truly one-time setup belongs in a setup project.

## Writing your own fixture[#](https://testingbot.com#writing-your-own-fixture)

Hooks get repetitive once five files need the same "open the app and prepare a helper" code. A custom fixture packages that setup once. Create `tests/fixtures.ts`:

```typescript
import { test as base, expect, type Locator, type Page } from '@playwright/test';

class TodoApp {
  readonly newTodo: Locator;
  readonly items: Locator;

  constructor(readonly page: Page) {
    this.newTodo = page.getByPlaceholder('What needs to be done?');
    this.items = page.getByTestId('todo-item');
  }

  async add(...titles: string[]) {
    for (const title of titles) {
      await this.newTodo.fill(title);
      await this.newTodo.press('Enter');
    }
  }
}

export const test = base.extend<{ todoApp: TodoApp }>({
  todoApp: async ({ page }, use) => {
    await page.goto('https://demo.playwright.dev/todomvc/');
    await use(new TodoApp(page));
    // anything after use() is teardown, and runs even if the test failed
  },
});

export { expect };
```

Tests import `test` from this file instead of from `@playwright/test`:

```typescript
import { test, expect } from './fixtures';

test('filters active items', async ({ todoApp, page }) => {
  await todoApp.add('Buy milk', 'Walk the dog');
  await todoApp.items
    .filter({ hasText: 'Buy milk' })
    .getByRole('checkbox', { name: 'Toggle Todo' })
    .check();

  await page.getByRole('link', { name: 'Active' }).click();
  await expect(todoApp.items).toHaveText(['Walk the dog']);
});
```

Why this beats a `beforeEach`:

- The setup only runs for tests that ask for `todoApp`.
- Setup and teardown sit next to each other, around `use()`.
- Fixtures can depend on other fixtures (`todoApp` depends on `page`), and the runner works out the order.
- TypeScript knows the type of `todoApp`, so you get autocompletion in every test.

`TodoApp` here is a small page object. If you want to design those properly, with components and navigation between pages, the [Page Object Model course](https://testingbot.com/resources/courses/page-object-model) covers the pattern in depth; the fixture shown here is the standard way to hand those objects to Playwright tests.

Two fixture options you will meet in other codebases: `{ scope: 'worker' }` creates the value once per worker process (for example a test account per worker), and `{ auto: true }` runs the fixture for every test even if no test names it, which is handy for global logging or for marking test results somewhere.

Your `todoApp` fixture creates a todo list through the API and should delete it after the test, even when the test fails. Where does the delete call go?

 In an `afterEach` in every spec file that uses the fixture Before `await use(...)`, so it is registered early After `await use(...)` inside the fixture In `afterAll`, so it runs once

Code after `use()` is the fixture's teardown and runs even if the test failed. Keeping setup and teardown together is the point of a fixture; code before `use()` would delete the list before the test sees it.

## Logging in once with a setup project[#](https://testingbot.com#logging-in-once-with-a-setup-project)

Logging in through the UI at the start of every test is slow and puts your login form on the critical path of every test. Playwright's answer is to log in once, save the browser context's cookies and local storage to a file, and start every test from that saved state.

The building blocks are a **setup project** and **storageState**. First, a setup file, `tests/auth.setup.ts`:

```typescript
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('log in once', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/login');
  await page.getByLabel('Username').fill(process.env.APP_USER ?? 'tomsmith');
  await page.getByLabel('Password').fill(process.env.APP_PASSWORD ?? 'SuperSecretPassword!');
  await page.getByRole('button', { name: 'Login' }).click();

  // Wait until login has really finished before saving cookies
  await expect(page.getByRole('heading', { name: 'Secure Area', exact: true })).toBeVisible();

  await page.context().storageState({ path: authFile });
});
```

(The fallback credentials are the public ones printed on that demo page. For your own app, read them from environment variables and never commit them.)

Then wire it into `playwright.config.ts`:

```typescript
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
  {
    name: 'firefox',
    use: { ...devices['Desktop Firefox'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
  {
    name: 'webkit',
    use: { ...devices['Desktop Safari'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],
```

`dependencies: ['setup']` guarantees the setup project finishes before any browser project starts. Dependent projects only run once every setup test has passed, so a broken login stops the run instead of producing a hundred confusing failures. Unlike the older `globalSetup` function, a setup project shows up in the HTML report and can record a trace, so a broken login is easy to debug.

Now a test simply starts logged in:

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

test('secure area opens without logging in again', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/secure');
  await expect(page.getByRole('link', { name: 'Logout' })).toBeVisible();
});

test.describe('logged out', () => {
  test.use({ storageState: { cookies: [], origins: [] } });

  test('secure area redirects to login', async ({ page }) => {
    await page.goto('https://the-internet.herokuapp.com/secure');
    await expect(page).toHaveURL(/\/login$/);
  });
});
```

`test.use` overrides an option for one file or `describe` block; here it gives the logged-out tests an empty state.

Three practical notes:

- The generated `.gitignore` already excludes `playwright/.auth/`. Keep it that way; the file holds live session cookies.
- If tests change server-side state for the user (settings, a cart), parallel tests sharing one account will collide. Use one account per worker (a worker-scoped fixture) or per role.
- To run a single spec without re-running login, `npx playwright test --no-deps` skips dependencies and uses the state file already on disk.

Someone breaks the login form. The browser projects declare `dependencies: ['setup']`. What does the run look like?

 The setup test fails and the dependent browser projects do not run Every test fails at the login step The browser projects run with an empty storage state and pass The browser projects reuse the last `user.json` on disk

Dependent projects only run once every setup test has passed, so you get one clear failure instead of a wall of confusing ones. Reusing the old state file only happens when you pass `--no-deps` yourself.

If the application under test is only reachable from your machine or private network, the browser still needs a route to it once you move to a cloud grid; our [local website testing course](https://testingbot.com/resources/courses/local-website-testing) explains how a tunnel handles that.

The suite now has reusable setup and an authenticated starting point. The next chapter is about what to do when one of these tests fails.

[Previous · Chapter 4 Auto-Waiting and Web-First Assertions](https://testingbot.com/resources/courses/playwright/auto-waiting-and-assertions) [Next · Chapter 6 Debugging: UI Mode, Trace Viewer, Codegen](https://testingbot.com/resources/courses/playwright/debugging-tests)

On this page

1. [The built-in fixtures](https://testingbot.com#the-built-in-fixtures)
2. [Hooks](https://testingbot.com#hooks)
3. [Writing your own fixture](https://testingbot.com#writing-your-own-fixture)
4. [Logging in once with a setup project](https://testingbot.com#logging-in-once-with-a-setup-project)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/playwright/certification)
