Когда пишешь async ({ page }) => {...} в тесте Playwright, ты получаешь фикстуру: Playwright создал свежую страницу браузера до запуска теста и автоматически закроет её после. Кастомные фикстуры работают так же: объявляются через test.extend() и получаются по имени в сигнатуре теста. Разница в том что setup и teardown определяешь ты сам, а разделителем служит await use(value). Это руководство покрывает все пять встроенных фикстур, паттерн test.extend() для кастомных фикстур, scope-опции для шаринга дорогостоящего setup между тестами и композицию кастомных фикстур.

Что такое фикстура

Фикстура: значение (или объект) которое Playwright подготавливает до запуска теста и очищает после. Dependency injection для тестов.

Вместо этого:

test('user can log in', async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // код теста
  
  await page.close();
  await context.close();
  await browser.close();
});

Пишешь вот так:

test('user can log in', async ({ page }) => {
  // page готова к использованию — setup и teardown управляются автоматически
});

Playwright управляет жизненным циклом. Каждый тест получает чистую page, которая автоматически закрывается после.

Встроенные фикстуры

Playwright предоставляет эти фикстуры из коробки:

| Фикстура | Тип | Что это |

|----------|-----|---------|

| page | Page | Новая страница браузера (вкладка) для каждого теста |

| browser | Browser | Экземпляр браузера (общий для тестов в рамках воркера) |

| context | BrowserContext | Браузерный контекст, как окно в режиме инкогнито |

| browserName | string | Текущий браузер: 'chromium', 'firefox', 'webkit' |

| request | APIRequestContext | HTTP-клиент для API-запросов |

page

Самая часто используемая фикстура. Каждый тест получает собственную изолированную страницу. После теста она автоматически закрывается.

test('page loads correctly', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await expect(page).toHaveTitle(/BecomeQA/);
});

context

Браузерный контекст работает как окно в режиме инкогнито: собственные куки, хранилище, сессия. Если в одном тесте нужны несколько страниц, создавай их из одного контекста:

test('two pages share the same session', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();
  
  await page1.goto('/login');
  // логинимся на page1
  
  // page2 тоже видит сессию (один контекст = одни куки)
  await page2.goto('/dashboard');
  await expect(page2.getByTestId('user-name')).toBeVisible();
});

browser

Напрямую browser нужен редко. Используй когда нужно создать контексты с конкретными настройками:

test('mobile viewport', async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 390, height: 844 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
  });
  const page = await context.newPage();
  await page.goto('/');
  // тест на мобильном вьюпорте
  await context.close();
});

request

Делает HTTP-запросы без браузера. Используется для API-тестирования и для подготовки тест-данных через API до UI-тестов.

test('create user via API', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: { email: 'new@test.com', password: 'ValidPass1' },
  });
  expect(response.status()).toBe(201);
});

browserName

Используй для условного пропуска тестов в конкретных браузерах:

test('file download', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'Download API different in Firefox');
  // ...
});

Кастомные фикстуры

В этом настоящая сила фикстур: ты создаёшь свои. Кастомные фикстуры работают точно как встроенные: объявляются один раз, используются везде через деструктуризацию.

Простая кастомная фикстура: страница после навигации

// fixtures/index.ts
import { test as base, expect } from '@playwright/test';

// определяем какие кастомные фикстуры существуют
type MyFixtures = {
  loggedInPage: Page;
};

export const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    // SETUP
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'ValidPass1');
    await page.click('[data-testid="submit"]');
    await page.waitForURL('/dashboard');
    
    // передаём тесту доступ к странице
    await use(page);
    
    // TEARDOWN (выполняется после теста)
    // ничего не нужно — страница закрывается автоматически
  },
});

export { expect };

// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';  // импортируй СВОЙ test, не из @playwright/test

test('dashboard shows welcome message', async ({ loggedInPage }) => {
  // уже залогинены — loggedInPage это страница после входа
  await expect(loggedInPage.getByTestId('welcome')).toBeVisible();
});

Кастомная фикстура для page object

Самый распространённый паттерн: фикстура которая предоставляет инициализированный класс page object.

// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type PageObjects = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
};

export const test = base.extend<PageObjects>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
});

// tests/login.spec.ts
import { test, expect } from '../fixtures';

test('successful login', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('user@test.com', 'ValidPass1');
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

Никаких ручных инстанций page objects в каждом тесте.

Кастомная фикстура с teardown

Если фикстура создаёт что-то требующее очистки:

type TestFixtures = {
  testUser: { id: number; email: string; token: string };
};

export const test = base.extend<TestFixtures>({
  testUser: async ({ request }, use) => {
    // SETUP: создаём пользователя
    const response = await request.post('/api/users', {
      data: {
        email: `test_${Date.now()}@example.com`,
        password: 'ValidPass1',
        role: 'member',
      },
    });
    const user = await response.json();
    
    // логинимся чтобы получить токен
    const loginResp = await request.post('/api/auth/login', {
      data: { email: user.email, password: 'ValidPass1' },
    });
    const { token } = await loginResp.json();
    
    // передаём тесту
    await use({ id: user.id, email: user.email, token });
    
    // TEARDOWN: удаляем пользователя
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});

test('user can update profile', async ({ page, testUser }) => {
  // testUser: id, email, token — свежие и уникальные для каждого теста
  await page.goto(`/users/${testUser.id}`);
  // ...
  // После теста пользователь удаляется автоматически
});

Scope фикстур

По умолчанию фикстуры имеют scope 'test': пересоздаются для каждого теста. Для дорогостоящих фикстур безопасных для шаринга можно выставить scope 'worker':

export const test = base.extend<{}, { sharedToken: string }>({
  sharedToken: [async ({ request }, use) => {
    // выполняется один раз на воркер, не на каждый тест
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
  }, { scope: 'worker' }],
});

Scope 'worker' подходит для того что дорого пересоздавать (наполнение БД, генерация файлов), только читается (токены аутентификации которые не изменяются) и безопасно шарить (нет состояния которое один тест может испортить для другого).

Композиция фикстур

Кастомные фикстуры могут зависеть от других фикстур (в том числе от других кастомных):

export const test = base.extend<{
  loginPage: LoginPage;
  authenticatedPage: Page;
}>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  // эта фикстура ИСПОЛЬЗУЕТ loginPage
  authenticatedPage: async ({ page, loginPage }, use) => {
    await loginPage.goto();
    await loginPage.login('user@test.com', 'ValidPass1');
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Чистая структура проекта

project/
├── fixtures/
│   └── index.ts        ← экспортирует твой расширенный test + expect
├── pages/
│   ├── LoginPage.ts
│   └── DashboardPage.ts
└── tests/
    ├── login.spec.ts   ← импортирует из fixtures/index.ts
    └── dashboard.spec.ts

Все тест-файлы импортируют из fixtures/index.ts, а не из @playwright/test напрямую. Так каждый тест автоматически получает доступ ко всем кастомным фикстурам.

Итог

| | Встроенные | Кастомные |

|-|------------|-----------|

| Где определены | Внутри Playwright | test.extend() в твоём коде |

| Где используются | Любой тест с { page }, { request } и т.д. | Любой тест через твой экспортированный test |

| Примеры | page, browser, request | loginPage, testUser, authToken |

| Жизненный цикл | Playwright управляет | Ты определяешь setup + await use() + teardown |

→ See also: Кастомные фикстуры в Playwright: паттерн, который делает тесты читаемыми | Структура тестов Playwright: describe, beforeEach, afterEach и хуки | Авторизация в Playwright через storageState (без логина в каждом тесте)