Когда пишешь 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 |