Несколько 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-набор, который растёт вместе с продуктом, а не превращается в обузу; а как делать сами проверки устойчивыми — утверждения и ожидание.