Проблема StaleReferenceException не нова. И сейчас стала в полный рост из-за бурного развития динамичных вебстраниц.
Для того чтобы понять как ее решать, нужно понять почему она возникает. Вернемся к истокам - как взаимодействуют языковые биндинги и собственно браузерный драйвер.
Все взаимодействие в вебдрайвере это клиент-серверное общение библиотек вебдрайвера для разных языков(клиент) и собственно браузерного драйвера (chromedriver, geckodriver …) - выступает в роли сервера. Общение происходит путем HTTP запросов от клиента к серверу. Все запросы реализованы по REST, и все общение стандартизировано. Набор команд которые должен выполнять драйвер чтобы быть совместимым с клиентом - описаны JSON Wire Protocol (JsonWireProtocol · SeleniumHQ/selenium Wiki · GitHub)
Представим ситуацию - мы хотим найти кнопку, кликнуть по ней, а после клика - наша кнопка перерендеривается на странице, а мы хотим кликнуть по ней опять.
В тестах это выглядит примерно так:
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
(JsonWireProtocol · SeleniumHQ/selenium Wiki · GitHub)
Все хорошо, запрос отработал, но наша кнопка теперь пропала из 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.