随机“元素不再附加到DOM” StaleElementReferenceException


143

我希望只是我一个人,但是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在发生异常之前添加一个实际的调用。但是,这是一个很差的解决方法,所以我希望有人指出我的错误,这将使一切变得更好。


26
希望17k的视图表明它不只是您;)这必须是那里最令人沮丧的Selenium异常。
Mark Mayo 2013年

4
48k!我有同样的问题……
2015年

3
我发现,硒是纯粹的,完全的垃圾....
ç约翰逊

4
60k,仍然是一个问题:)
Pieter De Bie'1

就我而言,这是因为这样做from selenium.common.exceptions import NoSuchElementException
CPT。Senkfuss

Answers:


119

是的,如果您在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();

presentOfElementLocated()方法如下所示:

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)

但是以我的经验,显式等待总是更可靠。


2
我是不是说不再可能将元素读入变量并重新使用它们?因为我有一个庞大的,动态的WATiR DSL,它依赖于传递的元素,因此我试图移植到webdriver,但是我遇到了同样的问题。本质上,对于更改DOM的每个测试步骤,我都必须添加代码以重新读取模块中的所有元素……
kinofrost 2011年

你好 请问在这个例子中函数是什么类型的?我似乎找不到....谢谢!
汉尼拔,

1
@汉尼拔:com.google.common.base.Function<F, T>,由番石榴提供。
Stephan202

@jarib,自您的解决方案提出以来,我也面临着同样的问题。问题是我正在用ruby编写脚本,并且没有名称为'presenceOfElementLocated'或类似名称的函数。有什么建议吗?
Amey 2012年

56
@jarib我不同意这都是由于测试设计不当造成的。因为即使在AJAX调用后元素出现之后,仍可能仍在运行jQuery代码,这可能导致StaleElementReferenceException。除了添加显式等待(看起来不太好)之外,您无能为力。我宁愿认为这是WebDriver中的一个设计缺陷
2012年

10

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

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认为过时的方式都有些懈怠。


7
您的代码中有一个错误,如果没有任何上限,您不应该继续递归地调用该方法,否则会造成堆栈崩溃。
哈利

2
我认为最好添加一个计数器或其他东西,因此当我们反复遇到错误时,实际上可以抛出错误。否则,如果确实存在错误,那么您将陷入循环
Sudara

我同意这不是书面测试不良的结果。Selenium倾向于在现代网站上执行此操作,即使是编写最佳的测试也是如此-可能是因为这些网站通过反应式Web应用程序框架中常见的双向绑定不断刷新其元素,即使没有更改也是如此。这些元素需要被制作。这样的方法应该成为测试现代Web应用程序的每个Selenium框架的一部分。
金刚砂

10

当AJAX更新正在进行时,有时会出现此错误。Capybara在等待DOM更改方面似乎很聪明(请参阅为什么将capitbara中的wait_until删除 ),但是在我的情况下,默认的2秒等待时间还不够。在_spec_helper.rb_中用例如

Capybara.default_max_wait_time = 5

2
这也解决了我的问题:我收到了StaleElementReferenceError并增加Capybara.default_max_wait_time解决了该问题。
brendan

1

我今天遇到了同样的问题,并组成了一个包装器类,该包装器类在每种方法之前检查元素引用是否仍然有效。我检索元素的解决方案非常简单,所以我想我会分享一下。

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变量中,并在需要时检索该元素。如果页面被重新加载,该参考将不再起作用。但是,只要对注定要做出改变,参考就不会消失。在大多数情况下,这应该可以胜任。

同样,它避免了重新搜索元素。

约翰


1

我有同样的问题,我的是旧硒版本引起的。由于开发环境,我无法更新到较新版本。该问题是由HTMLUnitWebElement.switchFocusToThisIfNeeded()引起的。当您导航到新页面时,您可能会在旧页面上单击的元素是oldActiveElement(请参见下文)。Selenium试图从旧元素获取上下文,但失败了。这就是为什么他们在将来的版本中建立了一个尝试。

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()举例。


1
哇...这真是个默默无闻的发现,不错的工作..我现在想知道其他驱动程序(例如chromedriver)是否也有类似问题
kevlarr

0

尝试将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

0

为了增加@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 for FireFox复制的)。该代码可用于通过以下方式避免过时的元素异常:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

上面的代码将driver.SpecificElementToGet().Displayed一直调用直到不driver.SpecificElementToGet()引发任何异常并且.Displayed计算结果为true5秒为止。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);
        }
}

0

我想我找到了处理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>

就这样。希望能帮助到你。


0

您可以通过使用显式等待来解决此问题,从而不必使用硬等待。

如果您使用一个属性获取所有元素并使用每个循环对其进行迭代,则可以在循环内部使用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 

-1

在Java 8中,您可以使用非常简单的方法

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

-5
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.

我在弄清楚之后才添加了它。抱歉,这是我第一次发布该格式。只是想帮助。如果您觉得有用,请与他人分享:)
Alvin Vera 2012年

欢迎来到stackoverflow!最好为示例代码提供简短的描述,以提高发布准确性:)
Picrofo Software 2012年

运行上面的代码可能会永远陷入循环,例如,如果该页面上出现服务器错误。
2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.