Случайный «Элемент больше не привязан к 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 там.
Марк Мэйо
4
48к сейчас! У меня та же проблема ...
Гал
3
Я нахожу, что Selenium - чистый и полный мусор ....
C Джонсон
4
60k, все еще проблема :)
Питер Де Би
в моем случае это было из-за выполненияfrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Ответы:

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

МетодsenceOfElementLocated () будет выглядеть примерно так:

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
Здравствуй. Могу ли я спросить, какой тип функции в этом примере? Я не могу найти это .... СПАСИБО!
Ганнибал
1
@Hannibal: com.google.common.base.Function<F, T>предоставлено Guava .
Stephan202
@jarib, я столкнулся с этой же проблемой через год после твоего решения. проблема в том, что я пишу свои скрипты на ruby, и нет функции с именем «senceOfElementLocated »или чем-то подобным. Любые рекомендации?
Amey
56
@jarib Я не согласен, это все из-за плохо разработанного теста. Поскольку даже после того, как элемент появляется после вызова AJAX, возможно, все еще выполняется код jQuery, который может вызвать исключение StaleElementReferenceException. И вы ничего не можете сделать, кроме как добавить явное ожидание, которое кажется не очень хорошим. Я скорее думаю, что это недостаток дизайна в WebDriver
munch
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 считает устаревшим, есть некоторая слабость.

aearon
источник
7
У вас есть ошибка в этом коде, вы не должны продолжать рекурсивно вызывать метод без какого-либо ограничения, иначе вы потеряете свой стек.
Гарри
2
Я думаю, что лучше добавить счетчик или что-то в этом роде, поэтому, когда мы неоднократно получаем ошибку, мы можем фактически выдать ошибку. В противном случае, если действительно произойдет ошибка, вы попадете в цикл
Sudara
Я согласен, что это не результат плохо написанных тестов. Selenium имеет тенденцию делать это на современных веб-сайтах, даже для самых хорошо написанных тестов, - вероятно, потому, что веб-сайты постоянно обновляют свои элементы с помощью двусторонних привязок, которые распространены в средах реагирующих веб-приложений, даже когда нет изменений в эти элементы должны быть сделаны. Подобный метод должен быть частью любой инфраструктуры Selenium, которая тестирует современное веб-приложение.
наждачная
10

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

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

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

Джон

Iwan1993
источник
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
ibaralf
источник
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 для 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

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

Если вы выбираете все элементы с одним свойством и просматриваете его, используя для каждого цикла, вы можете использовать ожидание внутри цикла следующим образом:

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.
Элвин Вера
источник
Я добавил это только сейчас, выяснив это. извините за формат это мой первый раз, чтобы опубликовать. Просто пытаюсь помочь. Если вы найдете это полезным, пожалуйста, поделитесь им с другими :)
Элвин Вера
Добро пожаловать в stackoverflow! Всегда лучше предоставить краткое описание примера кода для повышения точности публикации :)
Picrofo Software
Запустив приведенный выше код, вы можете навсегда застрять в цикле, если, например, на этой странице есть ошибка сервера.
Мун