Случайный "элемент больше не присоединяется к DOM" StaleElementReferenceException


Я надеюсь, что это только я, но 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 перед возникает исключение. Это плохое решение, хотя, поэтому я надеюсь, что кто-то может указать на ошибку с моей стороны, что сделает все это лучше.

10 128

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

метод 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 trunk есть переписанный драйвер Chrome, где большая часть реализации была выполнена разработчиками Chromium как часть их дерева.

PS. Кроме того, вместо явного ожидания, как в приведенном выше примере, вы можете включить неявное ожидание - таким образом, WebDriver всегда будет циклически работать до указанного времени ожидания, ожидающего появления элемента:

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

по моему опыту, явное ожидание всегда более надежно.

я смог использовать такой метод с некоторым успехом:

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 считает устаревшим.

Я получаю эту ошибку иногда, когда обновления AJAX находятся на полпути. Капибара, похоже, довольно умна в ожидании изменений DOM (см. почему wait_until был удален из капибары ), но время ожидания по умолчанию 2 секунд было просто недостаточно в моем случае. Изменено в _spec_helper.rb_, например,

Capybara.default_wait_time = 5

у меня была такая же проблема, и моя была вызвана старой версией Селена. Я не могу обновить до новой версии из-за среды разработки. Проблема вызвана HTMLUnitWebElement.switchFocusToThisIfNeeded (). При переходе на новую страницу может случиться так, что элемент, который вы нажали на старой странице, является oldActiveElement (см. ниже). Селен пытается получить контекст из старого элемента и терпит неудачу. Вот почему они построили try catch в будущих выпусках.

код selenium-htmlunit-версия драйвера

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() например.

я столкнулся с той же проблемой сегодня и составил класс-оболочку, который проверяет перед каждым методом, если ссылка на элемент все еще действительна. Мое решение для повторного использования элемента довольно простое, поэтому я подумал, что просто поделюсь им.

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 и извлекаю элемент, если это необходимо. Если страница будет перезагружена, эта ссылка больше не будет работать. Но пока вносятся только изменения, чтобы обреченность ссылки остается. И это должно делать свою работу в большинстве случаев.

также это позволяет избежать повторного поиска элемента.

Джон

просто случилось со мной при попытке send_keys в поле ввода поиска - это имеет autoupdate в зависимости от того, что вы вводите. Как упоминалось 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

чтобы добавить к ответу @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 для 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);
        }
}

Я думаю,что нашел удобный подход для обработки 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>

и это все. Надеюсь, это поможет.

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