← назад к разделу

Три теста написать легко. Пятьдесят — другой разговор. Без структуры e2e-набор быстро превращается в кашу из скопированного кода: в каждом тесте одинаковый логин, одни и те же локаторы разбросаны по десяткам файлов, а одна смена вёрстки ломает половину тестов сразу. Playwright решает это двумя инструментами — fixtures и page object model.

Изоляция по умолчанию

Сначала важный фундамент: каждый тест в Playwright работает в своём изолированном контексте браузера. Встроенная фикстура page — это свежие cookies, пустое хранилище, никаких следов от предыдущего теста. Тесты не должны зависеть друг от друга.

Это важно, потому что зависимый тест — ненадёжный тест. Если тест «создать заказ» работает только после теста «войти в систему», любой сбой в первом ломает весь хвост. Изоляция — главное, что держит набор стабильным.

Изоляция в браузере работает автоматически. Внешнее состояние (база данных, файлы) — ваша ответственность; о нём отдельно в статье данные и аутентификация.

Дублирование setup: откуда берётся и как победить

Типичная картина в молодом e2e-наборе:

test("видит свои заказы", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("user@example.com");
  await page.getByLabel("Пароль").fill("secret");
  await page.getByRole("button", { name: "Войти" }).click();
  // ... теперь сам тест
});

test("может изменить профиль", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("user@example.com");
  await page.getByLabel("Пароль").fill("secret");
  await page.getByRole("button", { name: "Войти" }).click();
  // ... теперь сам тест
});

Логин одинаковый — в каждом тесте. Это плохо: при смене формы авторизации придётся менять в десятках мест. Для этого существуют кастомные fixtures.

Fixtures: выносим общий setup один раз

Fixture (фикстура) — это именованный кусок setup-кода, который Playwright запускает перед тестом (и завершает после). Встроенных фикстур несколько: page, browser, context. Свои создают через test.extend.

import { test as base, type Page } from "@playwright/test";

type Fixtures = { authedPage: Page };

export const test = base.extend<Fixtures>({
  authedPage: async ({ page }, use) => {
    await page.goto("/login");
    await page.getByLabel("Email").fill("user@example.com");
    await page.getByLabel("Пароль").fill("secret");
    await page.getByRole("button", { name: "Войти" }).click();
    await use(page); // <-- здесь запускается тест
    // после use() — место для очистки, если нужна
  },
});

Теперь тесты выглядят так:

test("видит свои заказы", async ({ authedPage }) => {
  await authedPage.goto("/orders");
  await expect(authedPage.getByText("Мои заказы")).toBeVisible();
});

test("может изменить профиль", async ({ authedPage }) => {
  await authedPage.goto("/profile");
  await expect(authedPage.getByLabel("Имя")).toBeVisible();
});

Логин описан один раз, тесты читаются чисто. Меняется форма логина — правите в одном месте.

Фикстуры можно составлять: одна фикстура может зависеть от другой. Playwright разрешает зависимости сам.

Авторизация через storageState

Вход через настоящий UI в каждом тесте — медленно. Быстрее войти один раз, сохранить состояние браузера в файл (storageState) и восстанавливать его в тестах без нового логина. Это отдельная тема, подробно — в данные и аутентификация.

Page object model: прячем локаторы за понятными методами

Другая проблема роста набора: локаторы расползаются по всем тестам. Изменился data-testid кнопки — ищите по всем файлам.

Page object — это класс, который описывает одну страницу (или её часть): какие элементы на ней есть и какие действия можно совершить. Тест работает с методами класса, а не с локаторами напрямую.

class CheckoutPage {
  constructor(private readonly page: Page) {}

  async open() {
    await this.page.goto("/checkout");
  }

  async fillCard(number: string) {
    await this.page.getByLabel("Номер карты").fill(number);
  }

  async submit() {
    await this.page.getByRole("button", { name: "Оплатить" }).click();
  }

  get successMessage() {
    return this.page.getByText("Заказ оформлен");
  }
}
test("оформляет заказ", async ({ page }) => {
  const checkout = new CheckoutPage(page);
  await checkout.open();
  await checkout.fillCard("4242 4242 4242 4242");
  await checkout.submit();
  await expect(checkout.successMessage).toBeVisible();
});

Тест читается как сценарий, а не как набор технических команд. Изменилась вёрстка страницы оплаты — правите CheckoutPage, остальные тесты не трогаете.

Когда вводить fixtures, когда page objects

Не нужно строить всё сразу. Хороший ориентир:

Fixtures — когда один и тот же setup встречается в нескольких тестах. Даже двух хватит, чтобы вынести.

Page objects — когда несколько тестов работают с одной страницей и локаторы начинают повторяться.

Для первых одного-двух тестов обе абстракции могут быть лишними. Начинайте просто, добавляйте структуру по мере роста — не наоборот.

Коротко

  • Каждый тест в Playwright запускается в изолированном контексте браузера — тесты не должны зависеть друг от друга.
  • Кастомные fixtures создают через test.extend: код до use() — подготовка, после — очистка.
  • Fixtures убирают дублирование setup: логин, создание данных, любая повторяющаяся подготовка — один раз, не в каждом тесте.
  • Page object прячет локаторы и действия страницы за понятными методами; тест читается как сценарий.
  • Меняется вёрстка — правите page object, а не десятки тестов.
  • Вводите структуру по мере роста набора, не авансом.

Что почитать дальше

  • Локаторы в Playwright — как правильно находить элементы.
  • Данные и аутентификация — storageState и управление внешним состоянием.
  • Утверждения и ожидание — как делать проверки устойчивыми.