Общий алгоритм решения "element is not attached to page document"

selenium
webdriver
java
Теги: #<Tag:0x00007f3d44a5c480> #<Tag:0x00007f3d44a5c278> #<Tag:0x00007f3d44a5c098>

(Kosmos) #1

Добрый день всем.
Сталкиваюсь с вроде избитой темой "element is not attached to page document". Решение предлагают WebDriverWait + ExpectedConditions или Selenide.

Если брать 1е - то чего ожидать? Появления элемента на странице? Он же вроде был удален/перерисован. Или все правильно понимаю - ставить:

wait.until(ExpectedConditions.elementToBeClickable(xpath));

? Только вроде ошибка вылетает..


(Yaroslav Pernerovskyy) #2

А можно чуть подробнее контекст, какую ситуацию вам надо обработать?


(Oleksii Ihnatiuk) #3

Первое что попробуйте, это обновите ваш драйвер.


(Alexander) #4

Не джава, но найти аналогичные решения будет не сложно, думаю, тут ничего оригинального

IWait<IWebDriver> wait = new WebDriverWait(Driver, TimeSpan.FromMilliseconds(timeout));
wait.Until(driver => (bool) ((IJavaScriptExecutor) Driver).ExecuteScript("return jQuery.active == 0"));
wait.Until(driver => ((IJavaScriptExecutor) Driver).ExecuteScript("return document.readyState").Equals("complete"));
wait.Until(ExpectedConditions.ElementIsVisible(By.XPath(someSelector)));

(Oleksandr Khotemskyi) #5

Проблема StaleReferenceException не нова. И сейчас стала в полный рост из-за бурного развития динамичных вебстраниц.

Для того чтобы понять как ее решать, нужно понять почему она возникает. Вернемся к истокам - как взаимодействуют языковые биндинги и собственно браузерный драйвер.

Все взаимодействие в вебдрайвере это клиент-серверное общение библиотек вебдрайвера для разных языков(клиент) и собственно браузерного драйвера (chromedriver, geckodriver ...) - выступает в роли сервера. Общение происходит путем HTTP запросов от клиента к серверу. Все запросы реализованы по REST, и все общение стандартизировано. Набор команд которые должен выполнять драйвер чтобы быть совместимым с клиентом - описаны JSON Wire Protocol (https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol)

Представим ситуацию - мы хотим найти кнопку, кликнуть по ней, а после клика - наша кнопка перерендеривается на странице, а мы хотим кликнуть по ней опять.

В тестах это выглядит примерно так:
WebElement button = driver.findElement(By.cssSelector('.button'))

Как это будет выглядеть на уровне JSON Wire Protocol -
Улетает HTTP POST вида - /session/123/element с JSON body {"using": "css selector", "value": ".button"}. Нам прилетает в ответ JSON объект который описывает найденый элемент. В этом объекте находится уникальный идентификатор (UUID) DOM элемента, который собственно и является необходимым параметром для будущих запросов для этого элемента. Допустим нам вернулся айди - 123e4567-e89b-12d3-a456-426655440000

Дальше мы хотим сделать клик, в тестах мы делаем это где-то так button.click(). Внутри же это вызывает HTTP POST запрос, вида - /session/123/element/123e4567-e89b-12d3-a456-426655440000/click (https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidelementidclick)

Все хорошо, запрос отработал, но наша кнопка теперь пропала из DOM. Начинается самая мякотка.

Кликаем на кнопку опять - /session/123/element/123e4567-e89b-12d3-a456-426655440000/click но элемента с таким айди больше уже не существует, мы получаем StaleReferenceException - элемент когда то был, айдишник был валидный, но теперь элемента по такому айдишнику больше нет, и работать с ним соответсвенно нельзя.

Так организована сама структура JSON WireProtocol, запросы на работу с элементом уже не передают локаторы, они используют только айдишники. Соответственно драйвер даже если бы хотел бы - переискать элемент заново для нас не может, айдишника для этого недостаточно.

Общий алгоритм решения тут наверное только один - искать элемент сразу перед тем как вызывать на нем действия, и не хранить id элемента, а переискивать его каждый раз заново на странице.

Если мы используем голый selenium Webdriver - и не хотим писать свои обертки и методы для поиска и перепоиска элементов - то все что нам остается - хранить локаторы, и при каждом действии искать элемент заново путем повторных вызовов findElement
driver.findElement(BUTTON_LOCATOR).click()
driver.findElement(BUTTON_LOCATOR).click() //здесь мы уже переполучили новый айдишник, и потому StaleReferenceException не возникнет

Selenide (java), ProtractorJS, Selene (python) - предоставляют удобный механизм - свой враппер над WebElement, который будет делать http запрос на поиск элемента только тогда когда к нему реально будет идти обращение на манипуляцию (click, getText ...) - соответственно мы не получим устаревший айди. А при изменении айдишника - эти фреймворки автоматически пытаются переискать по локатору который был обьявлен для этого элемента - очень удобно. Такой подход хорошо себя зарекомендовал, и называется LazyElements.


element is not attached to the page document или как убедится. что элемент удален
(Oleksii Ihnatiuk) #6

Я бы дополнил это тем, что если вы используете парадигму PageObjects, то каждый ваш метод возвращает либо this либо новый объект страницы, и если вы знаете что после клика произойдет на странице какое-либо изменение, то можете просто вернуть new Page.


(Taras) #7

а если через @FindBy то переписать декоратор где FindBy елементи ищет и будет тоже переискивать (например 10 раз там или что то похоже)


(Oleksandr Khotemskyi) #8

Можете пожалуйста пояснить поподробнее? Как это поможет обойти StaleReferenceException?


(Oleksii Ihnatiuk) #9

A stale element reference exception появляется чаще всего в двух случаях по оф. документации:
- The element has been deleted entirely;
- The element is no longer attached to the DOM.

Это происходит когда до вашего поиска страница уже изменилась.
А при паттерне, когда вы после каждого действия, которое изменяет/обновляет и т.д. страницу передаете новый объект этой страницы - у вас всегда будет актуальное состояние страницы.


(Oleksandr Khotemskyi) #10

а что если элемент меняется в пределах выполнения одной функции в пейдж обджекте?


(Oleksii Ihnatiuk) #11

тогда скорей всего это можно разбить на несколько методов, или по факту это будет уже другой элемент (локатор же тоже будет другой)


(Oleksandr Khotemskyi) #12

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

Например - какой-то попап возникает по таймауту, лоадеры, интерактивные многопользовательские приложения.

Да и это требует использования аннотаций, пейдж фактори и усложняет создание самого обьекта.

Как по мне гораздо проще обрабатывать это на уровне самого элемента - спрятать всю логику по перепоиску элемента в обьекте обертке для вебэлемента. Сообственно как уже сделано в различных селениум фреймворках


(Oleksii Ihnatiuk) #13

Не все методы могут возвращать новый инстанс

тогда они возвращают this

методы возвращают что-то со страницы

тогда этот метод будет точно private и будет использоваться другим методом внутри этой страницы

какой-то попап возникает по таймауту, лоадеры, интерактивные многопользовательские приложения.

здесь, честно, не силен. Поэтому не могу поддержать дискуссию :slight_smile:

Как по мне гораздо проще обрабатывать это на уровне самого элемента - спрятать всю логику по перепоиску элемента в обьекте обертке для вебэлемента.

это просто будет еще одна обертка вот и все.


(Yaroslav Pernerovskyy) #14

В джаве можно заюзать StaleTolerantWebDriver decorator, который как раз и решает подобные проблемы, путем повтора поиска элемента, если словили StaleReferenceElementException


(Tural Badalov) #15

xotabu4, Может быть вы и сказали об этом, но мне кажется что нет. Вы рекомендуете перед каждым кликом производить новый поиск, но это не выход. У меня итак перед каждый кликом делался поиск. Допустим ситуация такая. ПОиск (нашли) . клик(грузит новая страница) ПОиск(нашли) клик (не проходит, потому что в прошлом ходе начали рано искать и нашли что то не то) . Т.е. получается так , что если страница полностью не прогрузила элементы и начался поиск, то она находит элемент, но чего то в нем не хватает (я не веб разработчик, без понятия что там происходит под капотом). Для меня решением стало ставить вручную ожидание перед поиском. Буду рад, если напишите, почему же всё таки элемент находится и чего в нем может не хватать для клика


(Oleksandr Khotemskyi) #16

Нет поиск так не работает.

Поиск элемента в вебдрайвере или вернет айди элемента, или ошибку.

  • Если нашли что-то не то (но все-равно же нашли что-то) значит локатор неточный
  • Если вернуло ошибку - до клика не дойдет

Конечно явные ожидания нужны. Особенно для современных SPA приложений


(Il'dar Valitov) #17

Респектище за такой доступный ликбез)

У меня проблема не решилась повторным поиском элемента по локатору. В некоторых случаях помогло обновление страницы через get.driver(url), перед поиском find.element. В некоторых случаях помогло обновление страницы ctrl+f5 через класс Actions.

Может быть дело в версии драйвера, а может в особенностях реализации самого веба, в викете. Еще не понятно)