Организация селекторов в классах

При автоматизации тестирования веб-приложений на WebdriverIO ключевым элементом является правильная организация селекторов. Селекторы — это адреса элементов на странице, с которыми взаимодействует тест. Их хаотичное использование усложняет поддержку тестов и делает код нестабильным. На практике часто применяют паттерн Page Object, который предполагает группировку селекторов и методов взаимодействия с ними в отдельные классы.

Создание классов для страниц

Каждая веб-страница или логический блок приложения представляется отдельным классом. Класс содержит:

  • селекторы элементов;
  • методы взаимодействия (клики, ввод текста, проверка состояния);
  • методы проверки состояния страницы (валидации, ожидания).

Пример структуры класса для страницы входа:

class LoginPage {
    // Селекторы
    get usernameInput() { return $('#username'); }
    get passwordInput() { return $('#password'); }
    get loginButton() { return $('button[type="submit"]'); }
    get errorMessage() { return $('.error-message'); }

    // Методы взаимодействия
    async enterUsername(username) {
        await this.usernameInput.setValue(username);
    }

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

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

    // Методы проверки
    async isErrorMessageDisplayed() {
        return await this.errorMessage.isDisplayed();
    }
}

module.exports = new LoginPage();

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

Разделение селекторов и действий

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

class DashboardPage {
    // Селекторы
    get userMenu() { return $('#user-menu'); }
    get logoutButton() { return $('#logout'); }

    // Действия
    async openUserMenu() {
        await this.userMenu.click();
    }

    async logout() {
        await this.openUserMenu();
        await this.logoutButton.click();
    }
}

Такой подход позволяет менять селекторы в одном месте без необходимости переписывать все методы, использующие элемент.

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

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

class TableComponent {
    constructor(tableId) {
        this.tableId = tableId;
    }

    getRow(index) {
        return $(`#${this.tableId} tr:nth-child(${index})`);
    }

    getCell(rowIndex, colIndex) {
        return $(`#${this.tableId} tr:nth-child(${rowIndex}) td:nth-child(${colIndex})`);
    }

    async clickCell(rowIndex, colIndex) {
        const cell = await this.getCell(rowIndex, colIndex);
        await cell.click();
    }
}

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

Локаторы с использованием атрибутов data-*

Для стабильности тестов важно использовать селекторы, независимые от структуры или стилей страницы. Наиболее надежными считаются селекторы по атрибутам data-test, data-testid и аналогичным:

get submitButton() { return $('[data-test="submit-button"]'); }

Преимущества:

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

Организация иерархии классов

Если страница состоит из повторяющихся компонентов (модальные окна, таблицы, формы), рекомендуется создавать вложенные классы компонентов, которые включаются в основной класс страницы:

class Modal {
    constructor(modalSelector) {
        this.modalSelector = modalSelector;
    }

    get closeButton() { return $(this.modalSelector + ' .close'); }
    get confirmButton() { return $(this.modalSelector + ' .confirm'); }

    async close() {
        await this.closeButton.click();
    }

    async confirm() {
        await this.confirmButton.click();
    }
}

class SettingsPage {
    constructor() {
        this.profileModal = new Modal('#profile-modal');
        this.privacyModal = new Modal('#privacy-modal');
    }
}

Такое разделение позволяет логически структурировать код, избегать дублирования и повышает читаемость тестов.

Конвенции именования селекторов

Для единообразия и удобства поддержки рекомендуется:

  • Использовать геттеры для всех селекторов;
  • Названия селекторов отражают смысл элемента, а не его визуальные атрибуты (submitButton, usernameInput, errorMessage);
  • Для повторяющихся компонентов использовать одинаковые имена методов и селекторов, чтобы тесты выглядели консистентно.

Ленивое получение элементов

Использование геттеров позволяет откладывать поиск элемента до момента обращения. Это снижает вероятность ошибок при перерендере страницы и ускоряет выполнение тестов.

get dynamicButton() { return $('#dynamic-button'); }

// Использование
await this.dynamicButton.click(); // поиск элемента происходит только здесь

Интеграция с тестами

Классы страниц используются напрямую в тестах, обеспечивая читаемость сценариев:

const LoginPage = require('../pages/LoginPage');

describe('Авторизация', () => {
    it('Проверка успешного входа', async () => {
        await browser.url('/login');
        await LoginPage.enterUsername('admin');
        await LoginPage.enterPassword('password');
        await LoginPage.clickLogin();

        // Ожидание появления элемента дашборда
        await expect($('#dashboard')).toBeDisplayed();
    });
});

Такой подход делает тесты компактными, логичными и устойчивыми к изменениям интерфейса.