【问题标题】:Random "Element is no longer attached to the DOM" StaleElementReferenceException随机“元素不再附加到 DOM”StaleElementReferenceException
【发布时间】:2011-04-18 21:35:34
【问题描述】:

我希望这只是我,但 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 的实际调用。虽然这是一个糟糕的解决方法,所以我希望有人能指出我的错误,这将使这一切变得更好。

【问题讨论】:

  • 希望 17k 的浏览量表明不只是你 ;) 这一定是最令人沮丧的 Selenium 异常。
  • 48k 现在!我也有同样的问题...
  • 我发现 Selenium 是纯粹的垃圾......
  • 60k,还是个问题 :)
  • 就我而言,这是因为from selenium.common.exceptions import NoSuchElementException

标签: java selenium-webdriver webdriver automated-tests


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

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

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

不过,根据我的经验,显式等待总是更可靠。

【讨论】:

  • 我是否正确地说不再可能将元素读入变量并重新使用它们?因为我有一个巨大的干燥和动态的 WATiR DSL,它依赖于传递元素,我正在尝试移植到 webdriver,但我遇到了同样的问题。本质上,我必须添加代码以重新读取模块中的所有元素,以便为每个更改 DOM 的测试步骤...
  • 嗨。请问这个例子中的函数是什么类型?我似乎找不到它....谢谢!
  • @Hannibal:com.google.common.base.Function&lt;F, T&gt;,由Guava提供。
  • @jarib,自从你的解决方案一年后,我面临同样的问题。问题是我正在用 ruby​​ 编写脚本,并且没有名为“presenceOfElementLocated”或类似名称的函数。有什么建议吗?
  • @jarib 我不同意这都是由于测试设计不当造成的。因为即使在 AJAX 调用之后元素出现之后,仍有可能导致 StaleElementReferenceException 的 jQuery 代码仍在运行。除了添加看起来不太好的显式等待之外,您无能为力。我宁愿认为这是 WebDriver 的设计缺陷
【解决方案2】:

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

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 认为陈旧的内容存在一些缺陷。

【讨论】:

  • 你在这段代码中有一个错误,你不应该在没有某种上限的情况下继续递归调用该方法,否则你会炸毁你的堆栈。
  • 我觉得还是加个计数器什么的比较好,这样当我们反复报错的时候,其实是可以抛出错误的。否则如果真的有错误,你最终会陷入循环
  • 我同意这不是写得不好的测试的结果。 Selenium 倾向于在现代网站上执行此操作,即使是编写最好的测试 - 可能是因为网站通过响应式 Web 应用程序框架中常见的双向绑定不断刷新其元素,即使没有更改需要制作这些元素。像这样的方法应该是每个测试现代 Web 应用程序的 Selenium 框架的一部分。
【解决方案3】:

我有时会在 AJAX 更新进行到一半时收到此错误。 Capybara 在等待 DOM 更改方面似乎非常聪明(请参阅Why wait_until was removed from Capybara ),但在我的情况下,默认等待时间 2 秒根本不够。在 _spec_helper.rb_ 中更改为例如

Capybara.default_max_wait_time = 5

【讨论】:

  • 这也解决了我的问题:我得到了一个 StaleElementReferenceError 并增加 Capybara.default_max_wait_time 解决了这个问题。
【解决方案4】:

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

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】:

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

    来自 selenium-htmlunit-driver 版本

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

    【讨论】:

    • 哇...这是一个非常不起眼的发现,干得好..我现在想知道其他驱动程序(例如 chromedriver)是否也有类似的问题
    【解决方案6】:

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

    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 的答案,我做了几个扩展方法来帮助消除竞争条件。

      这是我的设置:

      我有一个名为“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 评估为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】:

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

          如果您使用一个属性获取所有元素并使用 for 每个循环遍历它,您可以像这样在循环内使用等待,

          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 
          

          【讨论】:

            【解决方案10】:

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

            【讨论】:

              【解决方案11】:
              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.
              

              【讨论】:

              • 我弄明白后才加的。抱歉,这是我第一次发布的格式。只是想帮忙。如果你觉得有用,请分享给其他人:)
              • 欢迎来到stackoverflow!最好为示例代码提供简短描述以提高发布准确性:)
              • 运行上面的代码可能会永远卡在循环中,例如,如果该页面上存在服务器错误。
              猜你喜欢
              • 1970-01-01
              • 2012-12-17
              • 1970-01-01
              • 1970-01-01
              • 2014-07-15
              • 2014-06-06
              • 2014-10-05
              • 1970-01-01
              • 2021-08-05
              相关资源
              最近更新 更多