Stale Element обработка

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

Типичная ситуация: элемент был найден, сохранён в переменную, но между моментом поиска и использованием Angular перерисовал шаблон, заменив узел новым экземпляром. С точки зрения WebDriver ссылка на элемент устарела.


Как Protractor работает с DOM и почему это важно

Protractor построен поверх Selenium WebDriver, но добавляет собственную синхронизацию с Angular:

  • ожидание завершения $http запросов;
  • ожидание стабилизации $timeout;
  • автоматическая синхронизация с зоной Angular.

Однако Protractor не отслеживает жизненный цикл конкретных DOM-элементов. Он лишь знает, когда Angular «спокоен». Если Angular перерисовал компонент, старый DOM-узел уничтожается, а WebElement остаётся ссылкой на уже несуществующий объект.


Основные сценарии возникновения stale-элементов

Динамические списки

  • ngRepeat, *ngFor
  • сортировка, фильтрация, пагинация
  • live-обновление данных

Навигация без перезагрузки

  • ui-router
  • Angular Router
  • модальные окна и drawer-компоненты

Анимации и lazy-rendering

  • ngIf / *ngIf
  • условный рендеринг
  • отложенная инициализация компонентов

Асинхронные события

  • ожидание browser.sleep
  • использование then() без повторного поиска элемента

Антипаттерны, ведущие к Stale Element

Кэширование элементов

const submitButton = element(by.id('submit'));

submitButton.click();
// DOM обновился
submitButton.click(); // StaleElementReferenceError

Элемент был найден один раз и повторно использован после изменения DOM.

Использование elementFinder вне контекста действия

const rows = element.all(by.css('.row'));

rows.get(0).click();
// таблица обновилась
rows.get(1).click(); // устаревшая коллекция

ElementArrayFinder ленивый, но индексирование фиксирует ссылку на конкретный элемент.


Правильная стратегия: повторный поиск элементов

Ключевой принцип — не хранить WebElement дольше одного действия.

element(by.id('submit')).click();
element(by.id('submit')).click();

Каждый вызов element() заново ищет элемент в текущем DOM.


Использование ожиданий вместо sleep

browser.sleep() почти гарантирует проблемы со stale-элементами, так как DOM может измениться в любой момент.

Плохо

browser.sleep(2000);
element(by.css('.item')).click();

Правильно

const EC = protractor.ExpectedConditions;

const item = element(by.css('.item'));
browser.wait(EC.elementToBeClickable(item), 5000);
item.click();

Ожидание проверяет актуальность элемента перед действием.


Обработка stale-элементов через ExpectedConditions

Protractor автоматически перехватывает некоторые ошибки, но stale нужно обрабатывать явно.

const EC = protractor.ExpectedConditions;

browser.wait(() => {
  return element(by.css('.item')).isPresent();
}, 5000);

Каждая проверка выполняет новый поиск элемента, что исключает использование устаревшей ссылки.


Повтор действия с перехватом ошибки

Для нестабильных UI полезна стратегия повторной попытки.

async function safeClick(locator, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      await element(locator).click();
      return;
    } catch (e) {
      if (!e.name.includes('StaleElement')) throw e;
    }
  }
  throw new Error('Не удалось кликнуть элемент');
}

Такой подход особенно эффективен для:

  • автокомплитов
  • dropdown-меню
  • live-таблиц

Работа с коллекциями элементов

ElementArrayFinder обновляет список при каждом вызове, но результат get() — нет.

Неправильно

const rows = element.all(by.css('.row'));
const firstRow = rows.get(0);

firstRow.click();
// DOM обновился
firstRow.click(); // stale

Правильно

element.all(by.css('.row')).get(0).click();

Или через функцию:

function firstRow() {
  return element.all(by.css('.row')).get(0);
}

firstRow().click();

Stale Element и Page Object

В Page Object нельзя хранить ElementFinder как состояние, если DOM динамический.

Плохо

class Page {
  constructor() {
    this.saveButton = element(by.id('save'));
  }
}

Правильно

class Page {
  saveButton() {
    return element(by.id('save'));
  }
}

Каждый вызов возвращает актуальный элемент.


Влияние disableSynchronization

При browser.waitForAngularEnabled(false) Protractor теряет автоматическую синхронизацию, и stale-элементы возникают чаще.

В таких сценариях обязательны:

  • ExpectedConditions
  • явные ожидания
  • минимизация времени между поиском и действием

Диагностика проблем со stale-элементами

Полезные признаки:

  • ошибка появляется нерегулярно
  • тест падает только на CI
  • повторный запуск проходит успешно

Методы анализа:

  • логирование шагов до падения
  • временное добавление browser.takeScreenshot()
  • проверка Angular-хуков (ngIf, trackBy)

Использование trackBy в Angular как тестовое решение

При использовании ngRepeat / *ngFor без trackBy Angular пересоздаёт DOM-элементы при каждом изменении данных.

<div *ngFor="let item of items; trackBy: trackById">

Это снижает вероятность stale-элементов и делает UI более стабильным для тестов.


Когда stale-элемент — симптом архитектурной проблемы

Частые ошибки могут указывать на:

  • чрезмерные перерисовки компонентов
  • отсутствие trackBy
  • сложные chained-анимации
  • неконтролируемые подписки

В таких случаях исправление теста лишь маскирует проблему.


Ключевые принципы устойчивых тестов

  • Не хранить WebElement между действиями
  • Минимизировать время между поиском и использованием
  • Использовать ожидания, а не задержки
  • Повторно искать элемент при каждом действии
  • Проектировать Page Object без состояния DOM

Stale Element — не исключение, а нормальное следствие динамического UI. Устойчивость тестов достигается не обходами, а правильной моделью взаимодействия с DOM.