При автоматизации тестирования веб-приложений на 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();
});
});
Такой подход делает тесты компактными, логичными и устойчивыми к изменениям интерфейса.