it-roy-ru.com

Случайный "Элемент больше не привязан к DOM" StaleElementReferenceException

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

130
Ray Nicholus

Да, если у вас есть проблемы с 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)

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

114
jarib

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

Capybara.default_wait_time = 5
8
Eero

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

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 считает устаревшим, есть некоторая слабость.

8
aearon

У меня была такая же проблема, и моя была вызвана старой версией Selenium. Я не могу обновиться до более новой версии из-за среды разработки. Проблема вызвана 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
thug-gamer

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

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

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

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

Джон

0
Iwan1993

Это случилось со мной, когда я пытался отправить 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
0
ibaralf

Чтобы добавить ответ @ 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 оценивается как 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);
        }
}
0
Jared Beach