Page Object в Playwright в 2026: нужен ли вообще, и как обойтись без него
Page Object Pattern в e2e-тестах живёт с эпохи Selenium и WebDriver. Тогда без него тесты превращались в ад из CSS-селекторов и копипасты. В 2026 году с Playwright всё иначе: фреймворк уже даёт половину того, ради чего раньше городили POM. Но привычка осталась, и команды по инерции пишут трёхэтажные иерархии классов на каждую страницу.
Семь лет писала тесты в разных стеках. POM был полезен, потом стал балластом, потом снова пригодился, но в очень узком сценарии. Разберу, где он реально нужен в Playwright, а где это карго-культ.
Зачем POM появился
Идея простая: разметка меняется чаще, чем поведение пользователя. Если CSS-селектор зашит в десятке тестов, любой рефакторинг фронта красит весь набор. POM прячет селекторы в один класс, тесты говорят на языке действий: loginPage.login(email, password).
В мире Selenium это спасало. Локаторы были многословные, ожидания приходилось писать руками, типизации не было. Класс с методами — наименьшее зло.
Что Playwright делает за тебя
Playwright уже даёт автодождание, семантические локаторы и нормальную типизацию. Тест без POM выглядит так:
import { test, expect } from '@playwright/test';
test('логин по корректным кредам', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Пароль').fill('Hunter2!Hunter2');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(
page.getByRole('heading', { name: 'Личный кабинет' }),
).toBeVisible();
});
Никаких CSS, никаких ручных waits. Локаторы привязаны к ARIA-ролям, а не к классам. Если разработчик переименует класс кнопки, тест останется зелёным. Главный исторический аргумент за POM — «локаторы меняются» — на семантических локаторах работает гораздо слабее.
Когда POM мешает
На первых десяти тестах команда пишет POM «как в учебнике»: LoginPage, DashboardPage, SettingsPage. Дальше начинается:
- Методы превращаются в обёртки над одной строкой Playwright.
fillEmail(email)внутри делаетpage.getByLabel('Email').fill(email)— экономия нулевая, шум большой. - Появляются «составные» методы вида
loginAndOpenSettings(...). Тест становится непрозрачным: непонятно, что именно проверяется, где заканчивается setup и начинается assertion. - Иерархия классов разрастается.
BasePage,AuthorizedPage extends BasePage,SettingsPage extends AuthorizedPage. Через год никто не помнит, где живёт нужный метод. - Падает разделяемость. Один сценарий хочет нажать «Сохранить», другой — «Сохранить и закрыть». В POM появляются два почти одинаковых метода или один с булевым флагом.
Что зашло вместо POM
Я перешла на смесь из двух подходов: фикстуры Playwright для setup/teardown и helper-функции для повторяющихся последовательностей. Фикстура решает задачу «авторизованного контекста» лучше любого BasePage:
import { test as base, expect } from '@playwright/test';
type AuthFixture = {
authedPage: import('@playwright/test').Page;
};
export const test = base.extend<AuthFixture>({
authedPage: async ({ page, request }, use) => {
const email = `qa+${Date.now()}@example.com`;
await request.post('/api/test/users', {
data: { email, password: 'Hunter2!Hunter2' },
});
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Пароль').fill('Hunter2!Hunter2');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(
page.getByRole('heading', { name: 'Личный кабинет' }),
).toBeVisible();
await use(page);
},
});
export { expect };
В тесте теперь нет ни одной строки про логин:
import { test, expect } from './fixtures';
test('пользователь меняет имя в профиле', async ({ authedPage }) => {
await authedPage.getByRole('link', { name: 'Профиль' }).click();
await authedPage.getByLabel('Имя').fill('Елена');
await authedPage.getByRole('button', { name: 'Сохранить' }).click();
await expect(
authedPage.getByRole('status').filter({ hasText: 'Сохранено' }),
).toBeVisible();
});
Helper-функции беру для последовательностей, которые повторяются дословно в трёх и более тестах. Если функция вызывается в одном тесте — это просто строки в тесте, не надо городить.
Где POM всё-таки полезен
Не выбрасываю паттерн совсем. Он зашёл в трёх случаях:
Сложные виджеты с собственным состоянием
Календарь, drag-and-drop редактор, таблица с виртуализацией. У них десятки внутренних элементов и нетривиальные сценарии (пролистать неделю вперёд, открыть выпадашку, выбрать дату). Класс с методами уровня виджета здесь оправдан:
import type { Page, Locator } from '@playwright/test';
export class DateRangePicker {
private readonly root: Locator;
constructor(page: Page, label: string) {
this.root = page.getByRole('group', { name: label });
}
async open(): Promise<void> {
await this.root.getByRole('button', { name: /выбрать дату/i }).click();
}
async pickRange(from: string, to: string): Promise<void> {
await this.open();
await this.root.getByRole('gridcell', { name: from }).click();
await this.root.getByRole('gridcell', { name: to }).click();
}
}
Это не Page Object, это Component Object — паттерн ровно на тот узел, который реально сложный. На простые формы такое — оверкил.
Чужие участки UI
Внешний платёжный виджет, виджет авторизации SSO, разметка которого вне контроля команды. Здесь обёртка нужна, потому что версия виджета меняется без согласования с тобой, и удобнее иметь одну точку правки.
Когда у тестов несколько авторов с разным опытом
Если в команде три ручника, которые осваивают Playwright, тонкая обёртка над страницей помогает им писать тесты, не разбираясь во всех нюансах локаторов. Это про обучение, а не про архитектуру.
Что не зашло
- Иерархия
BasePage -> ChildPageчерез наследование. Тащит за собой случайные методы в дочерних классах. Композиция через фикстуры читается лучше. - Возврат
thisдля fluent API:page.fillEmail(...).fillPassword(...).submit(). Красиво в учебнике, на практике падения становятся менее наглядными: непонятно, на каком звене цепочки упало. - POM с встроенными ассертами:
page.assertLoggedIn(). Разница «делает» и «проверяет» размывается, тесты теряют семантику Arrange-Act-Assert. - Один POM на весь сайт. Видела «класс
Appна 800 строк». Тесты от него зависят все, любая правка ломает половину.
Чек-лист: нужен ли тебе POM
- У тебя 5–20 тестов? Не нужен. Семантические локаторы и фикстуры покрывают всё.
- Тесты повторяют 3–4 строки логина в начале? Не POM, а фикстура авторизованного контекста.
- Сложный календарь или редактор? Component Object на этот компонент — да.
- Класс
SomePageсостоит из методов в одну строку? Удали, перенеси действия в тесты. - В POM есть
expect(...)? Вынеси ассерты в тесты, иначе сложно понять, что проверяется.
Что в итоге
Полностью отказываться от POM — лозунг для статей. На практике он остаётся полезным паттерном для сложных компонентов и чужих виджетов. Но как «обязательная архитектура каждого Playwright-проекта» — устарел. Семантические локаторы, фикстуры и адресные helper-функции дают тот же выигрыш с меньшим количеством кода.
Перед тем как начинать новый проект с BasePage, попробуй неделю писать тесты прямо на page.getByRole. Если через эту неделю реальная боль будет, добавляй обёртки точечно. Поверх боли — гораздо честнее, чем поверх привычки.