StaleElementReferenceError возникает, когда тест
обращается к элементу DOM, который больше не существует в текущем
состоянии страницы. В контексте Protractor и Angular-приложений эта
проблема особенно распространена из-за асинхронного рендеринга,
двустороннего биндинга и частых обновлений DOM без полной перезагрузки
страницы.
Типичная ситуация: элемент был найден, сохранён в переменную, но между моментом поиска и использованием Angular перерисовал шаблон, заменив узел новым экземпляром. С точки зрения WebDriver ссылка на элемент устарела.
Protractor построен поверх Selenium WebDriver, но добавляет собственную синхронизацию с Angular:
$http запросов;$timeout;Однако Protractor не отслеживает жизненный цикл конкретных DOM-элементов. Он лишь знает, когда Angular «спокоен». Если Angular перерисовал компонент, старый DOM-узел уничтожается, а WebElement остаётся ссылкой на уже несуществующий объект.
Динамические списки
ngRepeat, *ngForНавигация без перезагрузки
ui-routerАнимации и lazy-rendering
ngIf / *ngIfАсинхронные события
browser.sleepthen() без повторного поиска
элементаconst submitButton = element(by.id('submit'));
submitButton.click();
// DOM обновился
submitButton.click(); // StaleElementReferenceError
Элемент был найден один раз и повторно использован после изменения DOM.
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.
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();
Ожидание проверяет актуальность элемента перед действием.
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('Не удалось кликнуть элемент');
}
Такой подход особенно эффективен для:
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();
В Page Object нельзя хранить ElementFinder как
состояние, если DOM динамический.
class Page {
constructor() {
this.saveButton = element(by.id('save'));
}
}
class Page {
saveButton() {
return element(by.id('save'));
}
}
Каждый вызов возвращает актуальный элемент.
При browser.waitForAngularEnabled(false) Protractor
теряет автоматическую синхронизацию, и stale-элементы возникают
чаще.
В таких сценариях обязательны:
ExpectedConditionsПолезные признаки:
Методы анализа:
browser.takeScreenshot()ngIf,
trackBy)При использовании ngRepeat / *ngFor без
trackBy Angular пересоздаёт DOM-элементы при каждом
изменении данных.
<div *ngFor="let item of items; trackBy: trackById">
Это снижает вероятность stale-элементов и делает UI более стабильным для тестов.
Частые ошибки могут указывать на:
trackByВ таких случаях исправление теста лишь маскирует проблему.
Stale Element — не исключение, а нормальное следствие динамического UI. Устойчивость тестов достигается не обходами, а правильной моделью взаимодействия с DOM.