Случайный "Элемент больше не привязан к DOM" StaleElementReferenceException
Я надеюсь, что это только я, но Selenium Webdriver кажется полным кошмаром. В настоящее время веб-репозиторий Chrome недоступен, а другие драйверы довольно ненадежны или, как кажется. Я борюсь со многими проблемами, но вот один.
Случайно, мои тесты не сработают с
"org.openqa.selenium.StaleElementReferenceException: Element is no longer attached
to the DOM
System info: os.name: 'Windows 7', os.arch: 'amd64',
os.version: '6.1', java.version: '1.6.0_23'"
Я использую webdriver версии 2.0b3. Я видел это с драйверами FF и IE. Единственный способ предотвратить это - добавить фактический вызов Thread.sleep
до возникновения исключения. Однако это плохой способ, поэтому я надеюсь, что кто-то может указать на ошибку с моей стороны, которая сделает все это лучше.
Ответы
Ответ 1
Да, если у вас возникли проблемы с StaleElementReferenceExceptions, потому что ваши тесты плохо написаны. Это состояние гонки. Рассмотрим следующий сценарий:
WebElement element = driver.findElement(By.id("foo"));
// DOM changes - page is refreshed, or element is removed and re-added
element.click();
Теперь, когда вы нажимаете на элемент, ссылка на элемент больше недействительна. Для WebDriver практически невозможно догадаться обо всех случаях, когда это может произойти, поэтому оно подбрасывает руки и дает вам контроль, кто, как автор теста/приложения, должен точно знать, что может или не может произойти. То, что вы хотите сделать, - это явно ждать, пока DOM находится в состоянии, когда вы знаете, что вещи не изменятся. Например, использование WebDriverWait для ожидания существования определенного элемента:
// times out after 5 seconds
WebDriverWait wait = new WebDriverWait(driver, 5);
// while the following loop runs, the DOM changes -
// page is refreshed, or element is removed and re-added
wait.until(presenceOfElementLocated(By.id("container-element")));
// now we're good - let click the element
driver.findElement(By.id("foo")).click();
Метод presenceOfElementLocated() будет выглядеть примерно так:
private static Function<WebDriver,WebElement> presenceOfElementLocated(final By locator) {
return new Function<WebDriver, WebElement>() {
@Override
public WebElement apply(WebDriver driver) {
return driver.findElement(locator);
}
};
}
Вы совершенно правы в том, что текущий драйвер Chrome довольно нестабилен, и вы с удовольствием услышите, что в Selenium trunk есть перезаписанный драйвер Chrome, где большая часть реализации была выполнена разработчиками Chromium как часть их дерево.
PS. В качестве альтернативы вместо ожидания явно, как в приведенном выше примере, вы можете включить неявные ожидания - таким образом, WebDriver всегда будет петлиться до тех пор, пока указанный тайм-аут не ожидает появления элемента:
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)
По моему опыту, явное ожидание всегда более надежное.
Ответ 2
Иногда я получаю эту ошибку, когда обновления AJAX находятся на полпути. Похоже, что Capybara довольно умен в ожидании изменений DOM (см. Почему wait_until был удален из Capybara), но в моем случае просто не хватило времени ожидания по умолчанию в 2 секунды. Изменено в _spec_helper.rb_ с помощью например
Capybara.default_max_wait_time = 5
Ответ 3
Я смог использовать такой метод с некоторым успехом:
WebElement getStaleElemById(String id) {
try {
return driver.findElement(By.id(id));
} catch (StaleElementReferenceException e) {
System.out.println("Attempting to recover from StaleElementReferenceException ...");
return getStaleElemById(id);
}
}
Да, он просто проводит опрос элемента до тех пор, пока он перестанет считаться устаревшим (свежим?). На самом деле это не доходит до корня проблемы, но я обнаружил, что WebDriver может быть довольно разборчивым в том, чтобы выбрасывать это исключение - иногда я получаю его, а иногда и нет. Или может быть, что DOM действительно меняется.
Поэтому я не совсем согласен с вышеприведенным ответом, что это обязательно указывает на плохо написанный тест. У меня есть свежие страницы, с которыми я никак не взаимодействовал. Я думаю, что есть некоторая шелушатость как в том, как представлен DOM, так и в том, что WebDriver считает устаревшим.
Ответ 4
У меня была та же проблема, и моя была вызвана старой версией селена. Я не могу обновить новую версию из-за среды разработки. Проблема вызвана HTMLUnitWebElement.switchFocusToThisIfNeeded(). Когда вы переходите на новую страницу, может случиться, что элемент, который вы нажали на старой странице, - это oldActiveElement
(см. Ниже). Selenium пытается получить контекст от старого элемента и терпит неудачу. Вот почему они построили попытку catch в будущих выпусках.
Код из версии selenium-htmlunit-driver < 2.23.0:
private void switchFocusToThisIfNeeded() {
HtmlUnitWebElement oldActiveElement =
((HtmlUnitWebElement)parent.switchTo().activeElement());
boolean jsEnabled = parent.isJavascriptEnabled();
boolean oldActiveEqualsCurrent = oldActiveElement.equals(this);
boolean isBody = oldActiveElement.getTagName().toLowerCase().equals("body");
if (jsEnabled &&
!oldActiveEqualsCurrent &&
!isBody) {
oldActiveElement.element.blur();
element.focus();
}
}
Код из версии selenium-htmlunit-driver >= 2.23.0:
private void switchFocusToThisIfNeeded() {
HtmlUnitWebElement oldActiveElement =
((HtmlUnitWebElement)parent.switchTo().activeElement());
boolean jsEnabled = parent.isJavascriptEnabled();
boolean oldActiveEqualsCurrent = oldActiveElement.equals(this);
try {
boolean isBody = oldActiveElement.getTagName().toLowerCase().equals("body");
if (jsEnabled &&
!oldActiveEqualsCurrent &&
!isBody) {
oldActiveElement.element.blur();
}
} catch (StaleElementReferenceException ex) {
// old element has gone, do nothing
}
element.focus();
}
Без обновления до версии 2.23.0 или новее вы можете просто указать любой элемент в фокусе страницы. Я просто использовал element.click()
, например.
Ответ 5
Сегодня я столкнулся с той же проблемой и составил класс-оболочку, который проверяет перед каждым методом, если ссылка на элемент все еще действительна. Мое решение для извлечения элемента довольно просто, поэтому я подумал, что просто передам его.
private void setElementLocator()
{
this.locatorVariable = "selenium_" + DateTimeMethods.GetTime().ToString();
((IJavaScriptExecutor)this.driver).ExecuteScript(locatorVariable + " = arguments[0];", this.element);
}
private void RetrieveElement()
{
this.element = (IWebElement)((IJavaScriptExecutor)this.driver).ExecuteScript("return " + locatorVariable);
}
Вы видите, что я "локализую" или скорее сохраняю элемент в глобальной переменной js и извлекаю элемент, если это необходимо. Если страница будет перезагружена, эта ссылка больше не будет работать. Но до тех пор, пока будут сделаны только изменения, обременительная ссылка остается. И это должно делать работу в большинстве случаев.
Также он избегает повторного поиска элемента.
Джон
Ответ 6
Только что случилось со мной при попытке send_keys в поле ввода поиска, которое имеет autoupdate в зависимости от того, что вы вводите. Как упоминалось Eero, это может произойти, если ваш элемент обновляет Ajax, когда вы вводите текст внутри входной элемент. Решение - отправить один символ за раз и снова выполнить поиск для элемента ввода. (Пример в рубине, показанный ниже)
def send_keys_eachchar(webdriver, elem_locator, text_to_send)
text_to_send.each_char do |char|
input_elem = webdriver.find_element(elem_locator)
input_elem.send_keys(char)
end
end
Ответ 7
Чтобы добавить к ответу @jarib, я сделал несколько методов расширения, которые помогают устранить условие гонки.
Вот моя настройка:
У меня есть класс Called "Driver.cs". Он содержит статический класс, полный методов расширения для драйвера и других полезных статических функций.
Для элементов, которые мне обычно нужно получить, я создаю метод расширения, например:
public static IWebElement SpecificElementToGet(this IWebDriver driver) {
return driver.FindElement(By.SomeSelector("SelectorText"));
}
Это позволяет вам извлечь этот элемент из любого тестового класса с кодом:
driver.SpecificElementToGet();
Теперь, если это приводит к StaleElementReferenceException
, у меня есть следующий статический метод в моем классе драйвера:
public static void WaitForDisplayed(Func<IWebElement> getWebElement, int timeOut)
{
for (int second = 0; ; second++)
{
if (second >= timeOut) Assert.Fail("timeout");
try
{
if (getWebElement().Displayed) break;
}
catch (Exception)
{ }
Thread.Sleep(1000);
}
}
Этот первый параметр функции - это любая функция, которая возвращает объект IWebElement. Второй параметр - это тайм-аут в секундах (код для таймаута был скопирован из Selenium IDE для FireFox). Код можно использовать для исключения исключения устаревшего элемента следующим образом:
MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);
Вышеприведенный код вызовет driver.SpecificElementToGet().Displayed
, пока driver.SpecificElementToGet()
не выдаст исключений, а .Displayed
примет значение true
и не пройдет 5 секунд. Через 5 секунд тест завершится с ошибкой.
С другой стороны, чтобы ждать, пока элемент не будет присутствовать, вы можете использовать следующую функцию следующим образом:
public static void WaitForNotPresent(Func<IWebElement> getWebElement, int timeOut) {
for (int second = 0;; second++) {
if (second >= timeOut) Assert.Fail("timeout");
try
{
if (!getWebElement().Displayed) break;
}
catch (ElementNotVisibleException) { break; }
catch (NoSuchElementException) { break; }
catch (StaleElementReferenceException) { break; }
catch (Exception)
{ }
Thread.Sleep(1000);
}
}
Ответ 8
Я думаю, что нашел удобный подход для обработки исключения StaleElementReferenceException. Обычно вам нужно написать оболочки для каждого метода WebElement, чтобы повторить действия, что разочаровывает и тратит много времени.
Добавление этого кода
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")));
if ((Boolean) ((JavascriptExecutor) webDriver).executeScript("return window.jQuery != undefined")) {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return jQuery.active == 0")));
}
перед каждым действием WebElement можно повысить стабильность ваших тестов, но вы все равно можете время от времени получать исключение StaleElementReferenceException.
Так вот что я придумал (используя AspectJ):
package path.to.your.aspects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteWebElement;
import org.openqa.selenium.support.pagefactory.DefaultElementLocator;
import org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Aspect
public class WebElementAspect {
private static final Logger LOG = LogManager.getLogger(WebElementAspect.class);
/**
* Get your WebDriver instance from some kind of manager
*/
private WebDriver webDriver = DriverManager.getWebDriver();
private WebDriverWait webDriverWait = new WebDriverWait(webDriver, 10);
/**
* This will intercept execution of all methods from WebElement interface
*/
@Pointcut("execution(* org.openqa.selenium.WebElement.*(..))")
public void webElementMethods() {}
/**
* @Around annotation means that you can insert additional logic
* before and after execution of the method
*/
@Around("webElementMethods()")
public Object webElementHandler(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* Waiting until JavaScript and jQuery complete their stuff
*/
waitUntilPageIsLoaded();
/**
* Getting WebElement instance, method, arguments
*/
WebElement webElement = (WebElement) joinPoint.getThis();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Object[] args = joinPoint.getArgs();
/**
* Do some logging if you feel like it
*/
String methodName = method.getName();
if (methodName.contains("click")) {
LOG.info("Clicking on " + getBy(webElement));
} else if (methodName.contains("select")) {
LOG.info("Selecting from " + getBy(webElement));
} else if (methodName.contains("sendKeys")) {
LOG.info("Entering " + args[0].toString() + " into " + getBy(webElement));
}
try {
/**
* Executing WebElement method
*/
return joinPoint.proceed();
} catch (StaleElementReferenceException ex) {
LOG.debug("Intercepted StaleElementReferenceException");
/**
* Refreshing WebElement
* You can use implementation from this blog
* http://www.sahajamit.com/post/mystery-of-stale-element-reference-exception/
* but remove staleness check in the beginning (if(!isElementStale(elem))), because we already caught exception
* and it will result in an endless loop
*/
webElement = StaleElementUtil.refreshElement(webElement);
/**
* Executing method once again on the refreshed WebElement and returning result
*/
return method.invoke(webElement, args);
}
}
private void waitUntilPageIsLoaded() {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")));
if ((Boolean) ((JavascriptExecutor) webDriver).executeScript("return window.jQuery != undefined")) {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return jQuery.active == 0")));
}
}
private static String getBy(WebElement webElement) {
try {
if (webElement instanceof RemoteWebElement) {
try {
Field foundBy = webElement.getClass().getDeclaredField("foundBy");
foundBy.setAccessible(true);
return (String) foundBy.get(webElement);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
} else {
LocatingElementHandler handler = (LocatingElementHandler) Proxy.getInvocationHandler(webElement);
Field locatorField = handler.getClass().getDeclaredField("locator");
locatorField.setAccessible(true);
DefaultElementLocator locator = (DefaultElementLocator) locatorField.get(handler);
Field byField = locator.getClass().getDeclaredField("by");
byField.setAccessible(true);
return byField.get(locator).toString();
}
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
Чтобы включить этот аспект, создайте файл src\main\resources\META-INF\aop-ajc.xml
и запишите
<aspectj>
<aspects>
<aspect name="path.to.your.aspects.WebElementAspect"/>
</aspects>
</aspectj>
Добавьте это к вашему pom.xml
<properties>
<aspectj.version>1.9.1</aspectj.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
</argLine>
</configuration>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</build>
И это все. Надеюсь, поможет.
Ответ 9
В Java 8 вы можете использовать очень простой метод для этого:
private Object retryUntilAttached(Supplier<Object> callable) {
try {
return callable.get();
} catch (StaleElementReferenceException e) {
log.warn("\tTrying once again");
return retryUntilAttached(callable);
}
}
Ответ 10
FirefoxDriver _driver = new FirefoxDriver();
// create webdriverwait
WebDriverWait wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
// create flag/checker
bool result = false;
// wait for the element.
IWebElement elem = wait.Until(x => x.FindElement(By.Id("Element_ID")));
do
{
try
{
// let the driver look for the element again.
elem = _driver.FindElement(By.Id("Element_ID"));
// do your actions.
elem.SendKeys("text");
// it will throw an exception if the element is not in the dom or not
// found but if it didn't, our result will be changed to true.
result = !result;
}
catch (Exception) { }
} while (result != true); // this will continue to look for the element until
// it ends throwing exception.