Проблемы с ожиданиями

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

Синхронное и асинхронное поведение

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

browser.waitForAngularEnabled(false);

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

Методы ожиданий

Protractor предоставляет несколько механизмов ожиданий:

  1. Implicit Waits (не рекомендуется) WebDriver позволяет установить глобальное время ожидания появления элементов:

    browser.manage().timeouts().implicitlyWait(5000);

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

  2. Explicit Waits (рекомендуется) Позволяют ожидать конкретное условие для конкретного элемента. Для этого используется класс ExpectedConditions:

    let EC = protractor.ExpectedConditions;
    let button = element(by.id('submit'));
    
    browser.wait(EC.elementToBeClickable(button), 5000, 'Кнопка не стала кликабельной за 5 секунд');

    Основные предопределённые условия:

    • presenceOf(element) — элемент присутствует в DOM;
    • visibilityOf(element) — элемент видим;
    • elementToBeClickable(element) — элемент доступен для клика;
    • textToBePresentInElement(element, text) — текст присутствует в элементе.
  3. Custom Expected Conditions Иногда стандартные условия не подходят, и требуется создать собственное условие:

    let customCondition = function(elem) {
        return elem.getText().then(text => text.includes('Готово'));
    };
    
    browser.wait(customCondition(element(by.id('status'))), 7000, 'Статус не обновился');

Основные проблемы и ошибки

  • Race conditions: когда тест пытается взаимодействовать с элементом до того, как он появился или стал интерактивным. Часто проявляется на страницах с динамической подгрузкой данных.
  • Фальшивые ошибки: использование sleep() вместо явных ожиданий. browser.sleep(5000) может «сработать» случайно, но приводит к нестабильности при изменении скорости загрузки.
  • Несовместимость с Angular и не-Angular страницами: автоматические ожидания работают только для Angular. Для других страниц необходимо использовать browser.wait() или ручную проверку состояния.

Рекомендации по управлению ожиданиями

  1. Избегать sleep(). Использование фиксированных задержек замедляет тесты и не гарантирует корректное выполнение.

  2. Использовать explicit waits для всех элементов, с которыми планируется взаимодействие.

  3. Разделять ожидания для разных типов действий:

    • ожидание появления элемента перед кликом;
    • ожидание изменения текста или состояния перед проверкой assert;
    • ожидание загрузки страницы перед взаимодействием с DOM.
  4. Комбинировать условия при необходимости, используя EC.and() или EC.or():

    browser.wait(
        EC.and(
            EC.visibilityOf(elem),
            EC.elementToBeClickable(elem)
        ),
        5000,
        'Элемент не видим и некликабелен'
    );
  5. Логирование времени ожидания помогает отследить проблемные участки теста и понять, почему тесты «подвисают» или падают.

Проблемы в асинхронной цепочке

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

// Неправильно
element(by.id('input')).sendKeys('тест');
expect(element(by.id('output')).getText()).toEqual('тест');

// Правильно
element(by.id('input')).sendKeys('тест').then(() => {
    expect(element(by.id('output')).getText()).toEqual('тест');
});

Использование async/await делает код более читаемым и безопасным:

await element(by.id('input')).sendKeys('тест');
const outputText = await element(by.id('output')).getText();
expect(outputText).toEqual('тест');

Практический пример: динамическая таблица

const EC = protractor.ExpectedConditions;
let row = element(by.cssContainingText('tr', 'Иванов'));

await browser.wait(EC.presenceOf(row), 5000, 'Строка Иванов не появилась в таблице');
await browser.wait(EC.visibilityOf(row), 5000, 'Строка Иванов не видна');
await row.element(by.css('.edit')).click();

В этом примере последовательные explicit waits гарантируют, что тест не упадет из-за того, что элемент ещё не добавлен в DOM или не видим пользователю.