Aleatorio "Elemento ya no está conectado al DOM" StaleElementReferenceException

143

Espero que sea solo yo, pero Selenium Webdriver parece una pesadilla completa. El controlador web Chrome actualmente no se puede usar, y los otros controladores son poco confiables, o eso parece. Estoy luchando contra muchos problemas, pero aquí hay uno.

Aleatoriamente, mis pruebas fallarán con un

"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'"

Estoy usando webdriver versiones 2.0b3. He visto que esto sucede con los controladores FF e IE. La única forma en que puedo evitar esto es agregar una llamada real Thread.sleepantes de que ocurra la excepción. Sin embargo, esa es una solución alternativa deficiente, así que espero que alguien pueda señalar un error de mi parte que mejorará todo esto.

Ray Nicholus
fuente
26
Con suerte, las vistas de 17k indican que no eres solo tú;) Esta tiene que ser la excepción de Selenium más frustrante que existe.
Mark Mayo
44
48k ahora! Tengo el mismo problema ...
Gal
3
Estoy descubriendo que el selenio es basura pura y completa ...
C Johnson
44
60k, sigue siendo un problema :)
Pieter De Bie
en mi caso fue por hacerfrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Respuestas:

119

Sí, si tiene problemas con StaleElementReferenceExceptions es porque sus pruebas están mal escritas. Es una condición de carrera. Considere el siguiente escenario:

WebElement element = driver.findElement(By.id("foo"));
// DOM changes - page is refreshed, or element is removed and re-added
element.click();

Ahora, en el punto donde hace clic en el elemento, la referencia del elemento ya no es válida. Es casi imposible para WebDriver hacer una buena suposición sobre todos los casos en que esto podría suceder, por lo que levanta las manos y le da el control a usted, que como autor de la prueba / aplicación debe saber exactamente qué puede o no suceder. Lo que desea hacer es esperar explícitamente hasta que el DOM esté en un estado en el que sepa que las cosas no cambiarán. Por ejemplo, usando un WebDriverWait para esperar a que exista un elemento específico:

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

El método presenciaOfElementoLocalizado () se vería así:

private static Function<WebDriver,WebElement> presenceOfElementLocated(final By locator) {
    return new Function<WebDriver, WebElement>() {
        @Override
        public WebElement apply(WebDriver driver) {
            return driver.findElement(locator);
        }
    };
}

Tiene toda la razón acerca de que el controlador Chrome actual es bastante inestable, y le alegrará saber que el tronco Selenium tiene un controlador Chrome reescrito, donde los desarrolladores de Chromium realizaron la mayor parte de la implementación como parte de su árbol.

PD. Alternativamente, en lugar de esperar explícitamente como en el ejemplo anterior, puede habilitar las esperas implícitas, de esta manera WebDriver siempre se repetirá hasta el tiempo de espera especificado esperando que el elemento esté presente:

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

Sin embargo, en mi experiencia, esperar explícitamente siempre es más confiable.

jarib
fuente
2
¿Estoy en lo cierto al decir que ya no es posible leer elementos en variables y reutilizarlos? Porque tengo un enorme DSL WATiR seco y dinámico que se basa en elementos que pasan y estoy tratando de portar a webdriver, pero tengo el mismo problema. Esencialmente tendré que agregar código para volver a leer todos los elementos del módulo para cada paso de prueba que altere el DOM ...
kinofrost
Hola. ¿Puedo preguntar qué tipo de función es en este ejemplo? Parece que no puedo encontrarlo ... ¡GRACIAS!
Hannibal
1
@Hannibal:, com.google.common.base.Function<F, T>proporcionado por Guava .
Stephan202
@jarib, estoy enfrentando este mismo problema un año desde su solución. El problema es que estoy escribiendo mis scripts en ruby, y no hay ninguna función con el nombre de 'presenciaDeElementoLocalizado' o algo similar. ¿Alguna recomendación?
Amey
56
@jarib No estoy de acuerdo, todo esto es causado por una prueba mal diseñada. Porque incluso después de que el elemento aparece después de una llamada AJAX, puede haber código jQuery aún en ejecución que podría causar la excepción StaleElementReferenceException. Y no hay nada que pueda hacer excepto agregar una espera explícita que no parece muy agradable. Prefiero pensar que este es un defecto de diseño en WebDriver
munch
10

He podido usar un método como este con cierto éxito:

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

Sí, solo sigue sondeando el elemento hasta que ya no se considera rancio (¿fresco?). Realmente no llega a la raíz del problema, pero he descubierto que WebDriver puede ser bastante exigente al lanzar esta excepción: a veces lo entiendo, y otras no. O podría ser que el DOM realmente está cambiando.

Así que no estoy del todo de acuerdo con la respuesta anterior de que esto necesariamente indica una prueba mal escrita. Lo tengo en páginas nuevas con las que no he interactuado de ninguna manera. Creo que hay algunas fallas en la forma en que se representa el DOM o en lo que WebDriver considera obsoleto.

aearon
fuente
77
Tiene un error en este código, no debe seguir llamando recursivamente al método sin algún tipo de límite o volará su pila.
Harry
2
Creo que es mejor agregar un contador o algo así, cuando recibimos el error repetidamente, en realidad podemos arrojar el error. De lo contrario, si realmente hay un error, terminarás en un bucle
Sudara
Estoy de acuerdo en que no es el resultado de exámenes mal escritos. Selenium tiende a hacer esto en los sitios web modernos, incluso para las pruebas mejor escritas, probablemente porque los sitios web están continuamente actualizando sus elementos a través de enlaces bidireccionales que son comunes en los marcos de aplicaciones web reactivos, incluso cuando no hay cambios en Esos elementos deben hacerse. Un método como este debería ser parte de cada marco de Selenium que prueba una aplicación web moderna.
esmerilado
10

A veces aparece este error cuando las actualizaciones de AJAX están a mitad de camino. Capybara parece ser bastante inteligente al esperar los cambios de DOM (consulte Por qué wait_until fue eliminado de Capybara ), pero el tiempo de espera predeterminado de 2 segundos simplemente no fue suficiente en mi caso. Cambiado en _spec_helper.rb_ con eg

Capybara.default_max_wait_time = 5
Eero
fuente
2
Esto también solucionó mi problema: estaba obteniendo un StaleElementReferenceError y al aumentar Capybara.default_max_wait_time resolvió el problema.
brendan
1

Hoy me enfrentaba al mismo problema e hice una clase de contenedor, que verifica antes de cada método si la referencia del elemento sigue siendo válida. Mi solución para recuperar el elemento es bastante simple, así que pensé en compartirlo.

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

Verá que "localizo" o, más bien, guardo el elemento en una variable global js y recupere el elemento si es necesario. Si la página se vuelve a cargar, esta referencia ya no funcionará. Pero siempre y cuando solo se hagan cambios para condenar, la referencia permanece. Y eso debería hacer el trabajo en la mayoría de los casos.

También evita volver a buscar el elemento.

Juan

Iwan1993
fuente
1

Tuve el mismo problema y el mío fue causado por una antigua versión de selenio. No puedo actualizar a una versión más nueva debido al entorno de desarrollo. El problema es causado por HTMLUnitWebElement.switchFocusToThisIfNeeded (). Cuando navega a una página nueva, puede suceder que el elemento en el que hizo clic en la página anterior sea el oldActiveElement(consulte a continuación). Selenium intenta obtener el contexto del elemento antiguo y falla. Es por eso que construyeron un intento de captura en futuras versiones.

Código de selenium-htmlunit-driver versión <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();
    }
}

Código de la versión del controlador selenio-htmlunit> = 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();
}

Sin actualizar a 2.23.0 o más reciente, puede asignar cualquier elemento al foco de la página. Acabo de usar element.click()por ejemplo.

gamberro
fuente
1
Wow ... Este fue un descubrimiento muy oscura, buen trabajo .. Ahora estoy preguntando si otros conductores (por ejemplo chromedriver) tienen problemas similares también
kevlarr
0

Me acaba de ocurrir cuando intento enviar_claves a un cuadro de entrada de búsqueda, que tiene una actualización automática según lo que escriba. Como mencionó Eero, esto puede suceder si su elemento actualiza Ajax mientras está escribiendo su texto dentro del elemento de entrada . La solución es enviar un carácter a la vez y buscar nuevamente el elemento de entrada . (Ej. En rubí se muestra a continuación)

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
fuente
0

Para agregar a la respuesta de @ jarib, he hecho varios métodos de extensión que ayudan a eliminar la condición de carrera.

He aquí mi arreglo:

Tengo una clase llamada "Driver.cs". Contiene una clase estática llena de métodos de extensión para el controlador y otras funciones estáticas útiles.

Para los elementos que normalmente necesito recuperar, creo un método de extensión como el siguiente:

public static IWebElement SpecificElementToGet(this IWebDriver driver) {
    return driver.FindElement(By.SomeSelector("SelectorText"));
}

Esto le permite recuperar ese elemento de cualquier clase de prueba con el código:

driver.SpecificElementToGet();

Ahora, si esto resulta en a StaleElementReferenceException, tengo el siguiente método estático en mi clase de controlador:

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

El primer parámetro de esta función es cualquier función que devuelve un objeto IWebElement. El segundo parámetro es un tiempo de espera en segundos (el código para el tiempo de espera se copió del Selenium IDE para FireFox). El código se puede usar para evitar la excepción del elemento obsoleto de la siguiente manera:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

El código anterior llamará driver.SpecificElementToGet().Displayedhasta que driver.SpecificElementToGet()no arroje excepciones y.Displayed evalúe truey no hayan pasado 5 segundos. Después de 5 segundos, la prueba fallará.

Por otro lado, para esperar a que un elemento no esté presente, puede usar la siguiente función de la misma manera:

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);
        }
}
Playa Jared
fuente
0

Creo que encontré un enfoque conveniente para manejar StaleElementReferenceException. Por lo general, debe escribir contenedores para cada método WebElement para volver a intentar acciones, lo cual es frustrante y desperdicia mucho tiempo.

Agregar este código

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

antes de que cada acción de WebElement pueda aumentar la estabilidad de sus pruebas, pero aún puede obtener StaleElementReferenceException de vez en cuando.

Entonces esto es lo que se me ocurrió (usando 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;
    }
}

Para habilitar este aspecto, cree un archivo src\main\resources\META-INF\aop-ajc.xml y escriba

<aspectj>
    <aspects>
        <aspect name="path.to.your.aspects.WebElementAspect"/>
    </aspects>
</aspectj>

Agregue esto a su 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>

Y eso es todo. Espero eso ayude.

Alexander Oreshin
fuente
0

Puede resolver esto usando la espera explícita para no tener que usar la espera dura.

Si obtiene todos los elementos con una propiedad e itera a través de ella usando cada ciclo, puede usar esperar dentro del ciclo de esta manera,

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
}

o para un solo elemento puede usar el siguiente código,

new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Your object property"));
driver.findElement("Your object property").click();//or anyother action 
Prajeeth Anand
fuente
-1

En Java 8 puede usar un método muy simple para eso:

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}
Łukasz Jasiński
fuente
-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.
Alvin Vera
fuente
Lo agregué justo ahora después de descubrirlo. perdón por el formato, esta es la primera vez que publico. Sólo trato de ayudar. Si lo encuentra útil, compártalo con otros :)
Alvin Vera
¡Bienvenido a stackoverflow! Siempre es mejor proporcionar una breve descripción de un código de muestra para mejorar la precisión de la publicación :)
Picrofo Software
Si ejecuta el código anterior, puede quedarse atrapado en el bucle para siempre, por ejemplo, si hay un error del servidor en esa página.
munch