Случайный "Элемент больше не привязан к 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'"
Я использую вебдрайвер версии 2.0b3. Я видел это с драйверами FF и IE. Единственный способ предотвратить это - добавить реальный вызов Thread.sleep
до возникновения исключения. Это плохой обходной путь, поэтому я надеюсь, что кто-то может указать на ошибку с моей стороны, которая сделает все это лучше.
10 ответов
Да, если у вас есть проблемы с 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's click the element
driver.findElement(By.id("foo")).click();
Метод senceOfElementLocated() будет выглядеть примерно так:
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 есть переписанный драйвер Chrome, где большая часть реализации была сделана разработчиками Chromium как часть их дерева.
PS. В качестве альтернативы, вместо явного ожидания, как в примере выше, вы можете включить неявное ожидание - таким образом, WebDriver всегда будет зацикливаться до тех пор, пока не истечет указанное время ожидания, пока элемент не появится:
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)
Однако, по моему опыту, явное ожидание всегда более надежно.
Иногда я получаю эту ошибку, когда обновления AJAX находятся на полпути. Похоже, что Capybara достаточно умен в ожидании изменений DOM (см. Почему wait_until был удален из Capybara), но в моем случае просто не хватило времени ожидания по умолчанию в 2 секунды. Изменено в _spec_helper.rb_ с помощью например
Capybara.default_wait_time = 5
Я смог использовать такой метод с некоторым успехом:
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 считает устаревшим, есть некоторая слабость.
Сегодня я столкнулся с той же проблемой и создал класс-оболочку, который перед каждым методом проверяет, является ли ссылка на элемент все еще действительной. Мое решение получить элемент довольно просто, поэтому я решил поделиться им.
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 и получу элемент, если это необходимо. Если страница будет перезагружена, эта ссылка больше не будет работать. Но до тех пор, пока вносятся только изменения, ссылка остается. И это должно делать работу в большинстве случаев.
Также это позволяет избежать повторного поиска элемента.
Джон
У меня была такая же проблема, и моя была вызвана старой версией селена. Я не могу обновиться до более новой версии из-за среды разработки. Проблема вызвана HTMLUnitWebElement.switchFocusToThisIfNeeded(). При переходе на новую страницу может случиться, что элемент, который вы щелкнули на старой странице, является oldActiveElement
(увидеть ниже). Selenium пытается получить контекст от старого элемента и терпит неудачу. Вот почему они создали попытку поймать в будущих выпусках.
Код из селена-htmlunit-драйвера версии < 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()
например.
Я думаю, что нашел удобный подход для обработки исключения 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>
И это все. Надеюсь, поможет.
Только что случилось со мной при попытке отправить send_keys в поле ввода поиска - оно имеет автообновление в зависимости от того, что вы вводите. Как упоминалось в 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
Чтобы добавить ответ @jarib, я сделал несколько методов расширения, которые помогают устранить условие гонки.
Вот моя установка:
У меня есть класс под названием "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);
}
}
Вы можете решить эту проблему, используя явное ожидание, чтобы вам не приходилось использовать жесткое ожидание.
Если вы выбираете все элементы с одним свойством и повторяете его, используя для каждого цикла, вы можете использовать ожидание внутри цикла следующим образом:
List<WebElement> elements = driver.findElements("Object property");
for(WebElement element:elements)
{
new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Object property"));
element.click();//or any other action
}
или для одного элемента вы можете использовать код ниже,
new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Your object property"));
driver.findElement("Your object property").click();//or anyother action
В Java 8 вы можете использовать очень простой метод для этого:
private Object retryUntilAttached(Supplier<Object> callable) {
try {
return callable.get();
} catch (StaleElementReferenceException e) {
log.warn("\tTrying once again");
return retryUntilAttached(callable);
}
}
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.