Инкапсуляция селекторов

Инкапсуляция селекторов — ключевой подход при организации автоматизированных тестов на Protractor. Она позволяет отделить логику работы с элементами страницы от самой структуры тестов, делая код более читаемым, поддерживаемым и удобным для масштабирования.

Принципы инкапсуляции

Основная идея заключается в том, чтобы сохранять все селекторы в одном месте — чаще всего в Page Object или отдельном объекте с элементами страницы. Это позволяет при изменении структуры интерфейса обновлять селекторы только в одном файле, без необходимости менять каждый тест отдельно.

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

  • Все селекторы должны быть приватными для объекта страницы. Тесты работают через методы, а не напрямую с локаторами.
  • Методы объекта страницы должны предоставлять логические действия, а не детали взаимодействия с элементами.
  • Селекторы можно хранить в виде переменных или геттеров, чтобы при обращении к элементу возвращался объект ElementFinder.

Page Object и структура селекторов

Классический пример инкапсуляции в Protractor выглядит так:

class LoginPage {
    constructor() {
        this.usernameInput = element(by.id('username'));
        this.passwordInput = element(by.css('input[type="password"]'));
        this.loginButton = element(by.buttonText('Login'));
        this.errorMessage = element(by.css('.error-msg'));
    }

    async enterUsername(username) {
        await this.usernameInput.clear();
        await this.usernameInput.sendKeys(username);
    }

    async enterPassword(password) {
        await this.passwordInput.clear();
        await this.passwordInput.sendKeys(password);
    }

    async clickLogin() {
        await this.loginButton.click();
    }

    async getErrorMessage() {
        return this.errorMessage.getText();
    }
}

В этом примере тесты никогда не используют селекторы напрямую. Они вызывают методы объекта страницы, что делает тесты лаконичными:

it('should show error for invalid login', async () => {
    const loginPage = new LoginPage();
    await loginPage.enterUsername('wronguser');
    await loginPage.enterPassword('wrongpass');
    await loginPage.clickLogin();
    const error = await loginPage.getErrorMessage();
    expect(error).toEqual('Invalid username or password');
});

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

Для улучшения инкапсуляции можно сделать локаторы приватными с использованием конвенций или синтаксиса ES6:

class LoginPage {
    #usernameInput = element(by.id('username'));
    #passwordInput = element(by.css('input[type="password"]'));
    #loginButton = element(by.buttonText('Login'));

    async enterUsername(username) {
        await this.#usernameInput.clear();
        await this.#usernameInput.sendKeys(username);
    }

    async enterPassword(password) {
        await this.#passwordInput.clear();
        await this.#passwordInput.sendKeys(password);
    }

    async clickLogin() {
        await this.#loginButton.click();
    }
}

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

Переиспользование селекторов

Для крупных проектов полезно создавать централизованные файлы с селекторами. Например:

// selectors.js
module.exports = {
    login: {
        usernameInput: by.id('username'),
        passwordInput: by.css('input[type="password"]'),
        loginButton: by.buttonText('Login'),
        errorMessage: by.css('.error-msg')
    },
    dashboard: {
        profileLink: by.css('.profile'),
        logoutButton: by.id('logout')
    }
};

Page Object использует эти селекторы:

const selectors = require('./selectors');

class LoginPage {
    usernameInput = element(selectors.login.usernameInput);
    passwordInput = element(selectors.login.passwordInput);
    loginButton = element(selectors.login.loginButton);

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

Такой подход упрощает поддержку при изменении идентификаторов на сайте и позволяет легко масштабировать проект, добавляя новые страницы и разделы интерфейса.

Динамические селекторы

Часто элементы страницы формируются динамически. Для их инкапсуляции можно использовать методы с параметрами:

class ProductPage {
    getProductCardByName(name) {
        return element(by.xpath(`//div[@class='product-card' and .//h2[text()='${name}']]`));
    }

    async clickAddToCart(productName) {
        const card = this.getProductCardByName(productName);
        await card.element(by.buttonText('Add to Cart')).click();
    }
}

Методы возвращают ElementFinder, а тесты остаются чистыми и не зависят от внутренней структуры страницы.

Преимущества инкапсуляции селекторов

  • Поддерживаемость: при изменении DOM достаточно обновить один метод или селектор.
  • Повторное использование: одни и те же методы можно использовать в разных тестах.
  • Читаемость: тесты описывают логические шаги, а не детали локаторов.
  • Масштабируемость: легко добавлять новые страницы, элементы и функциональность без дублирования кода.

Инкапсуляция селекторов — фундаментальная практика при работе с Protractor, которая повышает качество тестов, уменьшает технический долг и облегчает работу в крупных проектах.