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

时间:2011-04-18 21:35:35

标签: java selenium-webdriver webdriver automated-tests

我希望它只是我,但Selenium Webdriver似乎是一场彻头彻尾的噩梦。 Chrome webdriver目前无法使用,而其他驱动程序则非常不可靠,或者看起来如此。我正在与许多问题作斗争,但这里有一个问题。

随机地,我的测试将以

失败
"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添加实际调用。这是一个糟糕的解决方法,所以我希望有人可以指出我的错误,这将使这一切变得更好。

11 个答案:

答案 0 :(得分:115)

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

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后备箱有一个重写的Chrome驱动程序,其中大部分实现都是由Chromium开发人员完成的。树。

PS。或者,不要像上面的示例中那样明确地等待,而是可以启用隐式等待 - 这样WebDriver将始终循环直到指定的超时等待元素出现:

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

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

答案 1 :(得分:8)

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

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认为过时的方式都存在一些瑕疵。

答案 2 :(得分:8)

当AJAX更新处于中途时,我有时会收到此错误。 Capybara似乎非常聪明地等待DOM更改(参见Why wait_until was removed from Capybara ),但在我的情况下,默认等待时间2秒是不够的。改为_spec_helper.rb_,例如

Capybara.default_wait_time = 5

答案 3 :(得分:1)

我有同样的问题,我的是由旧的硒版本引起的。由于开发环境,我无法更新到更新的版本。问题是由HTMLUnitWebElement.switchFocusToThisIfNeeded()引起的。导航到新页面时,您在旧页面上单击的元素可能是oldActiveElement(见下文)。 Selenium试图从旧元素获取上下文并失败。这就是他们在未来版本中构建try catch的原因。

来自selenium-htmlunit-driver版本的代码&lt; 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版本的代码&gt; = 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()

答案 4 :(得分:0)

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

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

它也避免了重新搜索元素。

约翰

答案 5 :(得分:0)

尝试将send_keys发送到搜索输入框时发生了这种情况 - 根据您输入的内容自动更新。如Eero所述,如果您的元素在您输入文本时更新了一些Ajax,则会发生这种情况输入元素。解决方案是一次发送一个字符并再次搜索输入元素。 (例如下面显示的红宝石)

ValueError: could not broadcast input array from shape (294,3) into shape (294)

答案 6 :(得分:0)

要添加@ jarib的答案,我已经制作了几种扩展方法,有助于消除竞争条件。

这是我的设置:

我有一个名为&#34; Driver.cs&#34;的课程。它包含一个充满驱动程序扩展方法的静态类和其他有用的静态函数。

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

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

答案 7 :(得分: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>

仅此而已。希望对您有所帮助。

答案 8 :(得分:0)

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

如果您使用一个属性获取所有元素并使用每个循环对其进行迭代,则可以像这样在循环内部使用wait

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">


<PreferenceCategory
    android:title="@string/general"
    android:layout="@layout/fragment_settings">

    <Preference
        android:title="@string/about"
        android:icon="@drawable/ic_information"
        android:key="report_a_bug"
        android:summary="Find out more about Brainixx"/>

</PreferenceCategory>

<PreferenceCategory
    android:title="@string/brainixxapp"
    android:layout="@layout/fragment_settings">

    <Preference
        android:title="@string/bug"
        android:icon="@drawable/ic_bug"
        android:key="report_a_bug"
        android:summary="Hallo"/>

</PreferenceCategory>

或者对于单个元素,您可以使用以下代码

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
}

答案 9 :(得分:-1)

在Java 8中,你可以使用非常simple method

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

答案 10 :(得分:-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.