Page Object Model

Page Object Model (POM) — паттерн проектирования E2E тестов, при котором каждая страница приложения инкапсулируется в отдельный класс с методами для взаимодействия и селекторами.

Зачем нужно

Без POM каждый тест содержит дублирующиеся page.click('[data-testid="submit"]'). При изменении атрибута нужно обновлять десятки тестов. POM централизует селекторы и действия — один класс, один файл, одно место для обновления.

Где используется

  • Playwright и Cypress проекты с несколькими E2E тестами
  • Любой E2E suite где больше 3-5 тестов для одной страницы
  • Команды с разделением ролей QA и разработчик

Основной контент

Базовый Page Object на Playwright

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByTestId('email');
    this.passwordInput = page.getByTestId('password');
    this.submitButton = page.getByTestId('submit');
    this.errorMessage = page.getByTestId('error-message');
  }

  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 getError {
    return this.errorMessage.textContent;
  }
}

Использование в тестах

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

test('успешный вход', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardPage(page);

  await loginPage.goto;
  await loginPage.login('user@example.com', 'password123');

  await expect(page).toHaveURL('/dashboard');
  await expect(dashboardPage.welcomeText).toBeVisible;
});

test('неверный пароль', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto;
  await loginPage.login('user@example.com', 'wrong');

  expect(await loginPage.getError).toBe('Неверный пароль');
});

Связь между страницами — навигация

// pages/LoginPage.ts
async login(email: string, password: string): Promise<DashboardPage> {
  await this.emailInput.fill(email);
  await this.passwordInput.fill(password);
  await this.submitButton.click();
  return new DashboardPage(this.page); // возвращаем следующую страницу
}

// В тесте
const dashboard = await loginPage.login('user@example.com', 'pass');
await expect(dashboard.welcomeText).toBeVisible;

POM в Cypress

// cypress/pages/LoginPage.js
class LoginPage {
  visit { cy.visit('/login'); }
  fillEmail(email) { cy.get('[data-cy="email"]').type(email); }
  fillPassword(pwd) { cy.get('[data-cy="password"]').type(pwd); }
  submit { cy.get('[data-cy="submit"]').click(); }
  getError { return cy.get('[data-cy="error"]'); }
}
export const loginPage = new LoginPage();

Структура папок

tests/
  pages/
    LoginPage.ts
    DashboardPage.ts
    CheckoutPage.ts
  e2e/
    login.spec.ts
    checkout.spec.ts

Частые ошибки

  • Assertions внутри Page Object — POM должен только инкапсулировать взаимодействие; assertions оставляй в тестах
  • Один Page Object на всё приложение — большой класс со всеми методами; дели по страницам/компонентам
  • Дублирование логики между POM и тестами — если в тесте есть page.getByTestId(...), значит POM не используется полностью

Связанные темы

Ресурсы