Page Object Model строится на JavaScript-классах: класс LoginPage хранит локаторы в конструкторе, методы оборачивают взаимодействия, а через extends наследует общую навигацию от BasePage. Самая частая ошибка при изучении паттерна: забыть super() первой строкой конструктора дочернего класса, из-за чего локаторы родителя не настраиваются. В этом гайде: конструкторы, this, extends, super и статические методы, всё строится к полному примеру page object.
Что такое класс
Класс служит шаблоном для создания объектов. Каждый LoginPage созданный из класса получает одинаковые методы, но работает независимо.
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
}
async navigate() {
await this.page.goto('/login');
}
async login(email, password) {
await this.navigate();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Использование:
const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'ValidPass1');Конструктор
Конструктор запускается при создании нового экземпляра через new. Здесь инициализируются свойства.
class ProductPage {
constructor(page) {
// 'this' указывает на создаваемый экземпляр
this.page = page;
this.productTitle = page.getByTestId('product-title');
this.addToCartButton = page.getByTestId('add-to-cart');
this.price = page.getByTestId('product-price');
this.baseURL = '/products';
}
}
// При вызове new ProductPage(page):
// - конструктор запускается сразу
// - 'this.page' присваивается переданный аргумент page
// - локаторы сохраняются в экземпляре
const productPage = new ProductPage(page);this в классах
this указывает на текущий экземпляр. Через него методы обращаются к данным экземпляра.
class DashboardPage {
constructor(page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome');
this.userName = page.getByTestId('user-name');
this.logoutButton = page.getByTestId('logout');
}
async getUserName() {
// 'this.userName' - локатор сохранённый в конструкторе
return await this.userName.textContent();
}
async logout() {
// 'this.logoutButton' - локатор кнопки
await this.logoutButton.click();
}
async verifyUser(expectedName) {
// Методы могут вызывать другие методы через 'this'
const name = await this.getUserName();
if (name !== expectedName) {
throw new Error(`Expected ${expectedName}, got ${name}`);
}
}
}Методы
Функции определённые на классе называются методами. Могут быть синхронными или асинхронными.
class SearchPage {
constructor(page) {
this.page = page;
this.searchInput = page.getByTestId('search-input');
this.searchButton = page.getByTestId('search-button');
this.results = page.getByTestId('search-result');
this.noResultsMessage = page.getByTestId('no-results');
}
// Простой асинхронный метод
async search(query) {
await this.searchInput.fill(query);
await this.searchButton.click();
}
// Метод возвращающий значение
async getResultCount() {
return await this.results.count();
}
// Метод из нескольких шагов
async searchAndVerify(query, expectedCount) {
await this.search(query);
await this.page.waitForLoadState('networkidle');
const count = await this.getResultCount();
return count === expectedCount;
}
}Наследование через extends
Наследование позволяет одному классу строиться поверх другого. В POM обычно есть BasePage от которого наследуются все страницы.
// Базовый класс с общим поведением
class BasePage {
constructor(page) {
this.page = page;
this.header = page.getByTestId('header');
this.footer = page.getByTestId('footer');
}
async navigate(path) {
await this.page.goto(path);
}
async getTitle() {
return await this.page.title();
}
async takeScreenshot(name) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
}
// Дочерний класс наследует всё от BasePage
class LoginPage extends BasePage {
constructor(page) {
super(page); // Обязательно первой строкой: запускает конструктор BasePage
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
this.errorMessage = page.getByTestId('error-message');
}
async login(email, password) {
await this.navigate('/login'); // Унаследован от BasePage
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorText() {
return await this.errorMessage.textContent();
}
}Теперь LoginPage имеет и свои методы, и всё от BasePage:
const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'pass'); // метод LoginPage
await loginPage.takeScreenshot('after-login'); // метод BasePage
const title = await loginPage.getTitle(); // метод BasePagesuper: вызов родителя
super() вызывает конструктор родительского класса. Обязателен первой строкой в конструкторе дочернего класса.
class AdminPage extends BasePage {
constructor(page) {
super(page); // Запускает конструктор BasePage: устанавливает this.page, this.header, this.footer
// Теперь добавляем специфику AdminPage
this.userTable = page.getByTestId('users-table');
this.addUserButton = page.getByTestId('add-user');
}
}Родительские методы вызываются через super.methodName():
class CheckoutPage extends BasePage {
async navigate() {
// Вызываем родительский navigate, потом добавляем своё
await super.navigate('/checkout');
await this.page.waitForSelector('[data-testid="checkout-form"]');
}
}Статические методы
Статические методы принадлежат самому классу, а не экземплярам. Полезны для утилитарных функций.
class TestData {
static generateEmail() {
return `user_${Date.now()}@test.com`;
}
static generateUser() {
return {
email: TestData.generateEmail(),
password: 'ValidPass1',
name: 'Test User',
};
}
static generateProduct(overrides = {}) {
return {
name: 'Test Product',
price: 99.99,
category: 'electronics',
...overrides,
};
}
}
// Используешь без 'new': вызываешь прямо на классе
const email = TestData.generateEmail();
const user = TestData.generateUser();
const product = TestData.generateProduct({ price: 49.99 });Геттеры
Геттеры выглядят как свойства, но запускают функцию при обращении:
class CartPage extends BasePage {
constructor(page) {
super(page);
this.items = page.getByTestId('cart-item');
this.totalElement = page.getByTestId('cart-total');
}
get url() {
return '/cart'; // Вычисляемое свойство
}
async navigate() {
await super.navigate(this.url); // Использует геттер
}
async getItemCount() {
return await this.items.count();
}
}
const cartPage = new CartPage(page);
console.log(cartPage.url); // '/cart' (скобки не нужны)Полный пример page object
Всё вместе: реалистичный page object.
class ProductListPage extends BasePage {
constructor(page) {
super(page);
this.productCards = page.getByTestId('product-card');
this.filterDropdown = page.getByTestId('category-filter');
this.sortDropdown = page.getByTestId('sort-select');
this.searchInput = page.getByTestId('product-search');
this.loadingSpinner = page.getByTestId('loading');
}
async navigate() {
await super.navigate('/products');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async filterByCategory(category) {
await this.filterDropdown.selectOption(category);
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async sortBy(sortOption) {
await this.sortDropdown.selectOption(sortOption);
}
async search(query) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async getProductCount() {
return await this.productCards.count();
}
async getProductNames() {
return await this.productCards.getByTestId('product-name').allTextContents();
}
async clickProduct(index) {
await this.productCards.nth(index).click();
}
}// В тестах
test('фильтрация товаров по категории', async ({ page }) => {
const products = new ProductListPage(page);
await products.navigate();
await products.filterByCategory('electronics');
const count = await products.getProductCount();
expect(count).toBeGreaterThan(0);
});Класс vs объектный литерал
Для одноразового использования подойдёт объектный литерал. Для того что переиспользуется во многих тестах уместен класс.
// Объектный литерал: нормально для констант
const TEST_USERS = {
admin: { email: 'admin@test.com', password: 'AdminPass1' },
member: { email: 'member@test.com', password: 'MemberPass1' },
};
// Класс: хорошо для page objects используемых во многих тестах
class LoginPage extends BasePage {
// ...
}Краткое резюме
constructor(page): запускается приnew, сохраняй локаторы здесьthis: указывает на экземпляр; через него методы обращаются к данным объектаextends: наследование от другого класса (напримерBasePage)super(): вызов конструктора родителя; обязательная первая строка в дочернем конструктореstatic: методы уровня класса, экземпляр не нужен- Методы могут быть асинхронными: возвращают промисы, внутри используется
await