Когда сценариев становится десятки, встаёт вопрос, где живут локаторы и действия, чтобы правка интерфейса не заставляла переписывать половину набора. Классический ответ - Page Object, объект, инкапсулирующий работу со страницей. Но у паттерна есть тёмная сторона: он легко вырождается в свалку селекторов и методов на все случаи жизни. Хороший Page Object говорит на языке приложения, а не перечисляет элементы разметки.
Антипаттерн узнаваем - God Page Object. Один класс на всё приложение: login, buy, editProfile, openAdmin, и так на тысячи строк. Такой объект знает всё и потому меняется от любой правки, конфликтует в системе контроля версий и невозможен для навигации. Он повторяет ошибку монолита на уровне тестов: единая точка, в которую стекается вся сложность, вместо разделения по зонам ответственности.
Здоровая альтернатива - композиция маленьких объектов. Вместо одного всезнающего класса заводят несколько предметных: CheckoutPage для оформления, CartComponent для корзины, NavigationComponent для навигации, OrdersApi для работы с заказами через API. Каждый отвечает за свою зону, у каждого маленький понятный интерфейс. Сценарий собирает нужные объекты как кубики, а не тащит за собой один гигантский класс ради одного метода.
Внутри такой объект описывает действия на языке домена. CheckoutPage не выставляет наружу сырые локаторы, а даёт осмысленные методы: open() открывает страницу, payByCard(card) заполняет данные карты и нажимает оплату. Имя метода выражает пользовательское намерение - "оплатить картой", - а как именно это сделано в терминах полей и кнопок, спрятано внутри. Тест читается как бизнес-сценарий, а не как последовательность кликов.
Полезно один раз увидеть здоровый Page Object целиком, чтобы почувствовать меру. Ниже - CheckoutPage с конструктором, принимающим page, методом оплаты картой и методом, возвращающим локатор подтверждения. К этой форме возвращаются, когда объект страницы начинает разрастаться: если методов становится слишком много и они про разное, это сигнал разделить его на несколько предметных.
Тонкий вопрос - где держать проверки. Assertions можно оставлять в самом тесте, а не прятать в Page Object, и часто это правильно: тест тогда читается как история с видимыми ожиданиями, и понятно, что именно он утверждает. Component object при этом может возвращать локаторы наружу, чтобы тест сам делал проверку по ним. Разделение простое: объект даёт доступ и действия, тест выражает ожидания.
Есть и оправданное исключение - инкапсуляция сложного ожидания. Если дождаться доменного результата - это нетривиальная последовательность (дождаться статуса, затем появления записи, затем обновления счётчика), её разумно спрятать в метод объекта, но только когда имя метода честно выражает контракт. confirmation(), возвращающий локатор подтверждённого заказа, читается как обещание; безымянная свалка ожиданий - нет. Инкапсулируют смысл, а не прячут сложность под невнятным именем.
Типичные провалы архитектуры suite предсказуемы. God Page Object на тысячи строк, который меняется от всего и конфликтует у всех. Page Object как тонкая обёртка над сырыми селекторами без доменных методов - те же клики, только через лишний слой. И спрятанные в объект проверки, из-за которых по тесту не видно, что он утверждает. Дробите на предметные объекты, называйте методы намерением пользователя, а ожидания держите видимыми в тесте.
export class CheckoutPage {
constructor(private readonly page: Page) {}
async open() {
await this.page.goto('/checkout')
}
async payByCard(card: TestCard) {
await this.page.getByLabel('Номер карты').fill(card.number)
await this.page.getByRole('button', { name: 'Оплатить' }).click()
}
confirmation() {
return this.page.getByRole('status', { name: 'Заказ оплачен' })
}
}