随机的“ Element 不再附加到 DOM”StleElementReferenceException

我希望只有我这么觉得,但是 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.0 b3。我见过 FF 和 IE 驱动程序发生这种情况。防止这种情况发生的唯一方法是在异常发生之前添加对 Thread.sleep的实际调用。不过这是一个糟糕的解决方案,所以我希望有人能指出我的一个错误,这将使这一切更好。

125724 次浏览

是的,如果你遇到了 StleElementReferenceException 的问题,那是因为有一个竞争条件。考虑下面的场景:

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();

PresenceOfElementLocation ()方法如下所示:

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 开发人员作为他们的树的一部分完成的。

附言。或者,您可以启用隐式等待,而不是像上面的示例那样显式地等待——这样 WebDriver 将始终循环,直到指定的超时等待元素出现:

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)

不过根据我的经验,明确地等待总是更可靠的。

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.

我已经能够使用这样的方法,并取得了一些成功:

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);
}

您可以看到 i“ location”或者更确切地说,将元素保存在一个全局 js 变量中,并在需要时检索元素。如果页面被重新加载,这个引用将不再起作用。但是,只要只做了改变,注定参考停留。在大多数情况下,这应该可以解决问题。

此外,它还避免了对元素的重新搜索。

约翰

当 AJAX 更新进行到一半时,我有时会遇到这个错误。水豚在等待 DOM 更改方面似乎相当聪明(见 http://www.elabs.se/blog/53-Why-wait _ until-was-remove-from-Capybara”rel = “ nofollow noReferrer”> Why wait _ until was 负责从 Capybara 移除 ) ,但在我的情况下,2秒的默认等待时间根本不够。在 _ spec _ helper 中更改。例如:。

Capybara.default_max_wait_time = 5

在 Java8中,你可以使用非常 简单的方法:

private Object retryUntilAttached(Supplier<Object> callable) {
try {
return callable.get();
} catch (StaleElementReferenceException e) {
log.warn("\tTrying once again");
return retryUntilAttached(callable);
}
}

当我试图向搜索输入框发送 _ key 时,碰巧发生了这种情况——根据您输入的内容,该输入框具有自动更新功能。正如 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 的回答,我做了几个扩展方法来帮助消除竞态条件。

这是我的设置:

我有一门叫“司机”的课。它包含一个静态类,其中充满了驱动程序的扩展方法和其他有用的静态函数。

对于通常需要检索的元素,我创建一个扩展方法,如下所示:

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 for 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);
}
}

我也有同样的问题,而我的问题是由一个老版本的硒引起的。由于开发环境的原因,我无法更新到较新的版本。这个问题是由 HTMLUnitWebElement.switchFocusToThisIfNeeded ()引起的。当您导航到一个新页面时,可能会发生您在旧页面上单击的元素是 oldActiveElement(见下文)。Selenium 尝试从旧元素获取上下文,但是失败了。这就是为什么他们在未来的版本中构建了一个 try 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()作为例子。

我想我找到了一种方便的方法来处理 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>

就是这样,希望能有所帮助。

可以通过使用显式等待来解决这个问题,这样就不必使用硬等待了。

如果您获取具有一个属性的所有元素,并对每个循环使用它进行迭代,那么您可以像下面这样在循环中使用 wait,

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