Проблемы с локаторами

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


Надежность локаторов

Ключевой критерий локатора — его стабильность. Надежный локатор должен сохранять корректность даже при изменениях интерфейса, таких как редизайн или изменение структуры DOM. Проблемы возникают, когда:

  • Локатор зависит от позиции элемента, например element.all(by.css('div')).get(3). Изменение количества элементов в контейнере ломает тест.
  • Используются динамические идентификаторы (id="item_12345"), которые генерируются при каждой загрузке страницы.
  • Выбираются элементы по тексту, который может измениться (by.buttonText('Отправить')), особенно если текст локализован.

Решение: Использовать уникальные и стабильные атрибуты (data-*, ng-model для Angular) и избегать привязки к позициям или тексту, который может измениться.

Пример надежного локатора:

element(by.css('[data-test-id="login-button"]')).click();

Динамический контент и задержки

Protractor работает с Angular и умеет ждать завершения асинхронных операций. Однако проблемы с локаторами часто возникают при динамическом изменении DOM:

  • Элемент появляется на странице с задержкой. Если локатор выполняется слишком рано, возникает ошибка NoSuchElementError.
  • Элемент находится в списке, который подгружается частично (infinite scroll), и сразу получить нужный элемент невозможно.

Решение: Использовать ожидания (ExpectedConditions) для проверки доступности элемента:

const EC = protractor.ExpectedConditions;
const loginButton = element(by.css('[data-test-id="login-button"]'));
browser.wait(EC.visibilityOf(loginButton), 5000);
loginButton.click();

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


Сложные и длинные CSS-локаторы

Часто тестировщики создают локаторы типа:

element(by.css('body > app-root > app-login > div.container > form > button.btn-primary'))

Проблема заключается в:

  • Слишком глубокой зависимости от структуры DOM. Любое изменение в контейнерах ломает локатор.
  • Трудности поддержки и понимания кода.
  • Низкой читаемости тестов.

Решение: Использовать короткие и семантические локаторы с уникальными атрибутами или классами:

element(by.css('.login-form button.submit'));

или Angular-специфичные локаторы:

element(by.model('user.password'));

Локаторы по XPath

XPath позволяет создавать гибкие и точные выражения, однако их использование имеет подводные камни:

  • Медленная работа на больших страницах, особенно при использовании // для поиска по всему дереву.
  • Сложность чтения и поддержки. Например,
element(by.xpath('//div[@class="container"]/form//button[contains(text(), "Войти")]'))

может быть менее наглядным, чем CSS-селектор с уникальным атрибутом.

  • Чувствительность к структуре DOM. Любое изменение вложенности ломает XPath.

Решение: XPath использовать только там, где CSS невозможен или неэффективен. Для динамических текстов можно использовать функции contains() или starts-with(), но при этом сохранять минимальную глубину поиска:

element(by.xpath('//button[contains(@class,"submit") and text()="Войти"]'));

Смешивание локаторов и повторное использование

Частая ошибка — дублирование одинаковых локаторов по всему тестовому проекту. Это усложняет поддержку при изменении интерфейса.

Решение: Создавать отдельные Page Objects, где все локаторы определяются один раз:

class LoginPage {
    constructor() {
        this.username = element(by.model('user.username'));
        this.password = element(by.model('user.password'));
        this.loginButton = element(by.css('[data-test-id="login-button"]'));
    }

    login(user, pass) {
        this.username.sendKeys(user);
        this.password.sendKeys(pass);
        this.loginButton.click();
    }
}

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


Локаторы в повторяющихся элементах

Списки, таблицы и повторяющиеся компоненты создают дополнительные сложности:

  • Использование element.all(by.css(...)).get(index) может быть ненадежным.
  • Динамическое добавление и удаление элементов меняет индексы.

Решение: Использовать фильтры и условия для поиска конкретного элемента:

const rows = element.all(by.css('table tr'));
const userRow = rows.filter(async row => {
    const text = await row.getText();
    return text.includes('Иван Иванов');
}).first();
userRow.element(by.buttonText('Удалить')).click();

Такой метод позволяет не зависеть от индекса и правильно работать с динамическими списками.


Выводы по проблемам локаторов

Проблемы с локаторами чаще всего связаны с нестабильностью, зависимостью от структуры DOM или динамическим поведением страницы. Правильный подход включает:

  • Использование уникальных и семантических атрибутов (data-*, ng-model).
  • Минимизацию глубины CSS-селекторов и продуманное использование XPath.
  • Ожидания элементов через ExpectedConditions.
  • Организацию локаторов через Page Objects.
  • Фильтрацию и выборку элементов в списках вместо прямого индекса.

Эти методы значительно повышают устойчивость тестов и упрощают их сопровождение в больших проектах.