Page Object Model
Updated Jul 2026
Page Object Model (POM) — наиболее важный паттерн проектирования в автоматизации браузерного тестирования. Он разделяет «что тестировать» и «как взаимодействовать со страницей», делая тесты читаемыми и устойчивыми к изменениям UI. Паттерн одинаково применим к Selenium и Playwright, но API Playwright делает объекты страниц проще и мощнее.
Какую проблему решает POM
Без POM локаторы и взаимодействия разбросаны по тестовым файлам:
// ПЛОХО: локаторы дублируются в тестах
test('успешный вход', async ({ page }) => {
await page.locator('input[name="email"]').fill('user@test.com');
await page.locator('input[name="password"]').fill('pass123');
await page.locator('button.login-btn').click();
await expect(page.locator('h1.welcome')).toHaveText('Добро пожаловать');
});
test('неудачный вход', async ({ page }) => {
await page.locator('input[name="email"]').fill('bad@test.com');
await page.locator('input[name="password"]').fill('wrong');
await page.locator('button.login-btn').click();
await expect(page.locator('.error-message')).toBeVisible();
});
Если селектор поля email изменится, придётся обновить каждый тест. В наборе из 200 тестов это кошмар поддержки.
POM в Playwright (TypeScript)
// pages/login.page.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly submitButton: Locator;
private readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() ?? '';
}
}
Тесты с использованием POM
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test('успешный вход перенаправляет на панель', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'pass123');
await expect(page).toHaveURL(/.*dashboard/);
});
test('неверные учётные данные показывают ошибку', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('bad@test.com', 'wrong');
expect(await loginPage.getErrorMessage()).toContain('Invalid');
});
Если поле email изменится с <input> с меткой на пользовательский компонент, вы обновите одну строку в LoginPage — ноль тестовых файлов изменится.
Принципы POM
1. Один класс на страницу или крупный компонент
Каждая страница или значимый раздел UI получает свой класс. Не создавайте один гигантский класс AppPage.
2. Локаторы живут в объекте страницы, никогда в тесте
// ПЛОХО: локатор в тесте
await page.locator('#submit').click();
// ХОРОШО: локатор в объекте страницы
await checkoutPage.submitOrder();
3. Методы представляют действия пользователя, а не механику UI
Называйте методы по тому, что делает пользователь, а не что делает код:
// ХОРОШО: описывает намерение пользователя
await loginPage.login(email, password);
await productPage.addToCart();
await checkoutPage.applyCoupon('SAVE10');
// ПЛОХО: описывает механику UI
await loginPage.fillEmailAndPasswordAndClickSubmit(email, password);
4. Никаких утверждений внутри объектов страниц
Объекты страниц описывают, что страница может делать. Тесты решают, что проверять.
// ПЛОХО: утверждение внутри объекта страницы
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.submitButton.click();
await expect(this.page).toHaveURL(/dashboard/); // не делайте так
}
// ХОРОШО: объект страницы предоставляет действия, тест делает утверждения
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
POM в Playwright (Python)
# pages/login_page.py
class LoginPage:
def __init__(self, page):
self.page = page
self.email_input = page.get_by_label("Email")
self.password_input = page.get_by_label("Password")
self.submit_button = page.get_by_role("button", name="Sign in")
self.error_message = page.get_by_role("alert")
def goto(self):
self.page.goto("/login")
def login(self, email: str, password: str):
self.email_input.fill(email)
self.password_input.fill(password)
self.submit_button.click()
def get_error_message(self) -> str:
return self.error_message.text_content() or ""
Объекты компонентов
Переиспользуемые UI-компоненты, появляющиеся на нескольких страницах:
class HeaderComponent {
constructor(private page: Page) {}
async search(query: string) {
await this.page.getByRole('searchbox').fill(query);
await this.page.getByRole('searchbox').press('Enter');
}
async getCartCount(): Promise<number> {
const text = await this.page.getByTestId('cart-count').textContent();
return parseInt(text ?? '0');
}
}
class ProductPage {
readonly header: HeaderComponent;
constructor(private page: Page) {
this.header = new HeaderComponent(page);
}
async addToCart() {
await this.page.getByRole('button', { name: 'В корзину' }).click();
}
}
Ключевые выводы
- POM разделяет локаторы и взаимодействия (объекты страниц) от проверки (тесты)
- Один класс на страницу; локаторы в конструкторе; методы представляют действия пользователя
- Никаких утверждений внутри объектов страниц — тесты решают, что проверять
- API локаторов Playwright делает объекты страниц проще, чем эквиваленты Selenium (нет явных ожиданий в каждом методе)
- Используйте объекты компонентов для переиспользуемых элементов UI (шапка, подвал, модальные окна)
- Если локатор изменится, обновите одно место — а не каждый тест