Наследование Page Objects

Page Object — это шаблон проектирования для тестирования веб-приложений, который позволяет отделить логику тестов от структуры страниц, делая код более поддерживаемым и читаемым. В крупных проектах часто возникает необходимость переиспользовать общие элементы и методы между разными страницами. Для этого используется наследование Page Objects.


Основные принципы наследования

  1. Повторное использование кода Часто несколько страниц содержат одинаковые элементы: меню, кнопки навигации, футеры. Вместо копирования локаторов и методов в каждый Page Object, их можно вынести в базовый класс.

  2. Разделение ответственности Базовый Page Object отвечает за общие элементы и действия, а дочерние классы — за специфические для конкретной страницы функции.

  3. Упрощение поддержки Любое изменение в базовом элементе или методе автоматически отражается на всех страницах-наследниках.


Создание базового Page Object

Базовый класс обычно содержит элементы и методы, общие для всех страниц приложения:

class BasePage {
    constructor() {
        this.header = element(by.css('header'));
        this.footer = element(by.css('footer'));
        this.navigationMenu = element(by.css('.nav-menu'));
    }

    async goTo(url) {
        await browser.get(url);
    }

    async clickNavigationItem(itemText) {
        const item = this.navigationMenu.element(by.xpath(`.//a[text()='${itemText}']`));
        await item.click();
    }

    async isHeaderDisplayed() {
        return await this.header.isDisplayed();
    }
}

Ключевые моменты:

  • constructor хранит локаторы общих элементов.
  • Методы обеспечивают универсальные действия, которые будут доступны во всех наследниках.
  • Асинхронные функции используют await, что соответствует подходу Protractor с async/await.

Создание дочерних Page Objects

Дочерний класс наследует методы и элементы базового класса и добавляет специфические функции:

class LoginPage extends BasePage {
    constructor() {
        super();
        this.usernameInput = element(by.id('username'));
        this.passwordInput = element(by.id('password'));
        this.loginButton = element(by.buttonText('Login'));
    }

    async login(username, password) {
        await this.usernameInput.sendKeys(username);
        await this.passwordInput.sendKeys(password);
        await this.loginButton.click();
    }

    async isLoginButtonEnabled() {
        return await this.loginButton.isEnabled();
    }
}

Особенности наследования:

  • super() вызывает конструктор базового класса.
  • Дочерний класс может использовать все методы базового класса: goTo(), clickNavigationItem(), isHeaderDisplayed().
  • Локаторы и методы, специфичные для страницы, добавляются только здесь, что предотвращает дублирование кода.

Переопределение методов

Иногда требуется изменить поведение базового метода для конкретной страницы:

class DashboardPage extends BasePage {
    async clickNavigationItem(itemText) {
        console.log(`Переопределён метод для DashboardPage: ${itemText}`);
        await super.clickNavigationItem(itemText);
        await browser.sleep(500); // дополнительная задержка для динамических элементов
    }
}

Важно:

  • super.methodName() вызывает метод базового класса, сохраняя основную функциональность.
  • Переопределение полезно для добавления специфического поведения, не меняя базовый код.

Наследование нескольких уровней

В больших проектах можно создавать цепочку наследования:

class BasePage { /* общий функционал */ }

class AuthPage extends BasePage { /* общие элементы для страниц авторизации */ }

class LoginPage extends AuthPage { /* специфичные элементы для LoginPage */ }

class RegistrationPage extends AuthPage { /* элементы для регистрации */ }

Такой подход позволяет максимально структурировать проект, минимизируя повторение кода и улучшая поддержку.


Практические рекомендации

  • Выносить только действительно общие элементы: header, footer, меню, модальные окна.
  • Методы базового класса должны быть универсальными: не привязываться к конкретной логике страницы.
  • Использовать асинхронные функции для всех взаимодействий с элементами: это предотвращает проблемы с синхронизацией.
  • Переопределение методов только при необходимости: чтобы не запутать наследников.
  • Структурировать проект по иерархии Page Objects: это облегчает поиск элементов и упрощает расширение функционала.

Примеры использования

describe('Login tests', () => {
    const loginPage = new LoginPage();

    it('should log in successfully', async () => {
        await loginPage.goTo('/login');
        await loginPage.login('user1', 'password123');
        expect(await loginPage.isHeaderDisplayed()).toBe(true);
    });
});

describe('Dashboard tests', () => {
    const dashboardPage = new DashboardPage();

    it('should navigate to reports', async () => {
        await dashboardPage.goTo('/dashboard');
        await dashboardPage.clickNavigationItem('Reports');
    });
});

Преимущества наследования Page Objects

  • Централизация общих действий.
  • Снижение дублирования кода.
  • Улучшение читаемости и поддержки тестов.
  • Возможность легко расширять функционал страниц.

Наследование Page Objects превращает тестовую архитектуру в гибкую и масштабируемую систему, особенно полезную для крупных и динамичных веб-приложений.