Несколько e2e-тестов пишутся легко; пятьдесят — превращаются в свалку из copy-paste, если не задать структуру. Дублированный setup, повторяющиеся локаторы, запутанные зависимости между тестами — то, что делает большой e2e-набор неподдерживаемым. Playwright даёт два инструмента организации: fixtures и page objects.

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

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

Fixtures: общий setup

Когда нескольким тестам нужна одинаковая подготовка (залогиненный пользователь, готовые данные), её выносят в кастомную фикстуру через test.extend — а не повторяют в каждом тесте.

import { test as base } 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);
  },
});
test("видит свои заказы", async ({ authedPage }) => {
  await authedPage.goto("/orders");
  // уже залогинен
});

Код до use(page) — подготовка, после — очистка. Фикстуры компонуются и переиспользуются; вход через настоящий UI медленный, поэтому для авторизации чаще берут storageState, а фикстуры оставляют для прикладного setup.

Page object model

Когда сценарии сложные, локаторы и действия страницы инкапсулируют в page object — класс, прячущий «как» за осмысленными методами.

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

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

  async pay(card: string) {
    await this.page.getByLabel("Номер карты").fill(card);
    await this.page.getByRole("button", { name: "Оплатить" }).click();
  }
}
test("оформляет заказ", async ({ page }) => {
  const checkout = new CheckoutPage(page);
  await checkout.open();
  await checkout.pay("4242 4242 4242 4242");
  await expect(page.getByText("Заказ оформлен")).toBeVisible();
});

Тест читается на языке сценария, а локаторы спрятаны в одном месте — меняется вёрстка, правишь page object, а не десять тестов.

Когда что

Не усложнять преждевременно. Для пары простых тестов хватает фикстур и локаторов прямо в тесте. Page objects вводят, когда сценарий повторяется и локаторы расползаются по тестам. POM «на всякий случай» с первого дня — то же преждевременное усложнение, что лишний слой в backend.

Где это в UCP

Структура e2e — это управление сложностью набора: изоляция держит тесты независимыми, fixtures убирают дублирование setup, page objects прячут «как» за «что». Это те же принципы — одна ответственность, не повторяйся, не усложняй авансом, — что держат чистым backend-код. Для продукт-инженера это e2e-набор, который растёт вместе с продуктом, а не превращается в обузу; а как делать сами проверки устойчивыми — утверждения и ожидание.