lenec ru

← все посты

Page Object в Playwright в 2026: нужен ли вообще, и как обойтись без него

16K

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

  1. У тебя 5–20 тестов? Не нужен. Семантические локаторы и фикстуры покрывают всё.
  2. Тесты повторяют 3–4 строки логина в начале? Не POM, а фикстура авторизованного контекста.
  3. Сложный календарь или редактор? Component Object на этот компонент — да.
  4. Класс SomePage состоит из методов в одну строку? Удали, перенеси действия в тесты.
  5. В POM есть expect(...)? Вынеси ассерты в тесты, иначе сложно понять, что проверяется.

Что в итоге

Полностью отказываться от POM — лозунг для статей. На практике он остаётся полезным паттерном для сложных компонентов и чужих виджетов. Но как «обязательная архитектура каждого Playwright-проекта» — устарел. Семантические локаторы, фикстуры и адресные helper-функции дают тот же выигрыш с меньшим количеством кода.

Перед тем как начинать новый проект с BasePage, попробуй неделю писать тесты прямо на page.getByRole. Если через эту неделю реальная боль будет, добавляй обёртки точечно. Поверх боли — гораздо честнее, чем поверх привычки.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.