Aléatoire "L'élément n'est plus attaché au DOM" StaleElementReferenceException


143

J'espère que ce n'est que moi, mais Selenium Webdriver semble être un cauchemar complet. Le pilote Web Chrome est actuellement inutilisable et les autres pilotes ne sont pas assez fiables, du moins il semble. Je suis aux prises avec de nombreux problèmes, mais en voici un.

Au hasard, mes tests échoueront avec 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'"

J'utilise les versions 2.0b3 de Webdriver. J'ai vu cela se produire avec les pilotes FF et IE. La seule façon d'éviter cela est d'ajouter un appel réel Thread.sleepavant que l'exception ne se produise. C'est une solution de contournement médiocre, donc j'espère que quelqu'un pourra signaler une erreur de ma part qui améliorera tout cela.


26
Espérons que les 17k vues indiquent que ce n'est pas seulement vous;) Cela doit être l'exception Selenium la plus frustrante qui soit.
Mark Mayo

4
48k maintenant! J'ai le même problème ...
Gal

3
Je trouve que le sélénium est une poubelle pure et complète ....
C Johnson

4
60k, encore un problème :)
Pieter De Bie

dans mon cas, c'était à cause de fairefrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Réponses:


119

Oui, si vous rencontrez des problèmes avec StaleElementReferenceExceptions, c'est parce que vos tests sont mal écrits. C'est une condition de course. Considérez le scénario suivant:

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

Désormais, au moment où vous cliquez sur l'élément, la référence d'élément n'est plus valide. Il est presque impossible pour WebDriver de faire une bonne estimation de tous les cas où cela pourrait se produire - il lève donc les mains et vous donne le contrôle, qui en tant qu'auteur du test / de l'application devrait savoir exactement ce qui peut ou non se produire. Ce que vous voulez faire est d'attendre explicitement que le DOM soit dans un état où vous savez que les choses ne changeront pas. Par exemple, en utilisant un WebDriverWait pour attendre qu'un élément spécifique existe:

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

La méthode presenceOfElementLocated () ressemblerait à ceci:

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

Vous avez tout à fait raison sur le fait que le pilote Chrome actuel est assez instable, et vous serez heureux d'apprendre que le tronc Selenium a un pilote Chrome réécrit, où la plupart de l'implémentation a été effectuée par les développeurs Chromium dans le cadre de leur arbre.

PS. Alternativement, au lieu d'attendre explicitement comme dans l'exemple ci-dessus, vous pouvez activer les attentes implicites - de cette façon, WebDriver bouclera toujours jusqu'au délai spécifié d'attente pour que l'élément devienne présent:

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

D'après mon expérience, l'attente explicite est toujours plus fiable.


2
Ai-je raison de dire qu'il n'est plus possible de lire des éléments dans des variables et de les réutiliser? Parce que j'ai un énorme WATiR DSL sec et dynamique qui repose sur le passage d'éléments et que j'essaie de porter sur un pilote Web, mais j'ai le même problème. Essentiellement, je vais devoir ajouter du code pour relire tous les éléments du module pour chaque étape de test qui modifie le DOM ...
kinofrost

salut. Puis-je demander quel type de fonction est dans cet exemple? Je n'arrive pas à le trouver ... MERCI!
Hannibal

1
@Hannibal:, com.google.common.base.Function<F, T>fourni par Guava .
Stephan202

@jarib, je suis confronté à ce même problème un an depuis votre solution. le problème est que j'écris mes scripts en ruby, et il n'y a pas de fonction du nom de «presenceOfElementLocated» ou quelque chose de similaire. Des recommandations?
Amey

56
@jarib Je ne suis pas d'accord, tout cela est dû à un test mal conçu. Parce que même après l'apparition de l'élément après un appel AJAX, il se peut que du code jQuery soit toujours en cours d'exécution, ce qui pourrait provoquer l'exception StaleElementReferenceException. Et vous ne pouvez rien faire à part ajouter une attente explicite qui ne semble pas très agréable. Je pense plutôt que c'est un défaut de conception dans WebDriver
grignotez

10

J'ai pu utiliser une méthode comme celle-ci avec un certain succès:

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

Oui, il continue d'interroger l'élément jusqu'à ce qu'il ne soit plus considéré comme périmé (frais?). Cela ne va pas vraiment à la racine du problème, mais j'ai trouvé que le WebDriver peut être assez pointilleux pour lancer cette exception - parfois je l'obtiens, et parfois non. Ou il se pourrait que le DOM change vraiment.

Je ne suis donc pas tout à fait d'accord avec la réponse ci-dessus selon laquelle cela indique nécessairement un test mal rédigé. Je l'ai sur de nouvelles pages avec lesquelles je n'ai en aucune façon interagi. Je pense qu'il y a une certaine fragilité dans la façon dont le DOM est représenté, ou dans ce que WebDriver considère comme obsolète.


7
Vous avez un bogue dans ce code, vous ne devriez pas continuer à appeler la méthode de manière récursive sans une sorte de plafond ou vous ferez exploser votre pile.
Harry

2
Je pense qu'il est préférable d'ajouter un compteur ou quelque chose du genre, donc lorsque nous recevons l'erreur à plusieurs reprises, nous pouvons en fait lancer l'erreur. Sinon s'il y a effectivement une erreur, vous vous retrouverez dans une boucle
Sudara

Je conviens que ce n'est pas le résultat de tests mal écrits. Selenium a tendance à le faire sur les sites Web modernes, même pour les tests les mieux écrits - probablement parce que les sites Web actualisent continuellement leurs éléments via les liaisons bidirectionnelles qui sont courantes dans les frameworks d'applications Web réactives, même si aucune modification n'est apportée à ces éléments doivent être faits. Une méthode comme celle-ci devrait faire partie de chaque framework Selenium qui teste une application Web moderne.
emery

10

J'obtiens parfois cette erreur lorsque les mises à jour AJAX sont à mi-chemin. Capybara semble être assez intelligent pour attendre les changements du DOM (voir Pourquoi wait_until a été supprimé de Capybara ), mais le temps d'attente par défaut de 2 secondes n'était tout simplement pas suffisant dans mon cas. Changé dans _spec_helper.rb_ avec eg

Capybara.default_max_wait_time = 5

2
Cela a également résolu mon problème: j'obtenais une StaleElementReferenceError et l'augmentation de Capybara.default_max_wait_time a résolu le problème.
brendan le

1

J'étais confronté au même problème aujourd'hui et j'ai créé une classe wrapper, qui vérifie avant chaque méthode si la référence de l'élément est toujours valide. Ma solution pour récupérer l'élément est assez simple, alors j'ai pensé que je voulais simplement la partager.

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

Vous voyez je "localise" ou plutôt enregistre l'élément dans une variable js globale et récupère l'élément si nécessaire. Si la page est rechargée, cette référence ne fonctionnera plus. Mais tant que seuls des changements sont apportés pour vaincre, la référence demeure. Et cela devrait faire l'affaire dans la plupart des cas.

Cela évite également de rechercher à nouveau l'élément.

John


1

J'ai eu le même problème et le mien était causé par une ancienne version au sélénium. Je ne peux pas mettre à jour vers une version plus récente en raison de l'environnement de développement. Le problème est dû à HTMLUnitWebElement.switchFocusToThisIfNeeded (). Lorsque vous accédez à une nouvelle page, il se peut que l'élément sur lequel vous avez cliqué sur l'ancienne page soit le oldActiveElement(voir ci-dessous). Selenium essaie d'obtenir le contexte de l'ancien élément et échoue. C'est pourquoi ils ont construit un essai dans les versions futures.

Code de la version du pilote selenium-htmlunit <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();
    }
}

Code de la version 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();
}

Sans mettre à jour vers 2.23.0 ou une version plus récente, vous pouvez simplement donner n'importe quel élément sur le focus de la page. Je viens de l'utiliser element.click()par exemple.


1
Wow ... C'était une découverte vraiment obscure, du bon travail ... Je me demande maintenant si d'autres pilotes (par exemple chromedriver) ont également des problèmes similaires
kevlarr

0

Je viens de m'arriver en essayant d'envoyer des touches à une zone de saisie de recherche - qui a une mise à jour automatique en fonction de ce que vous tapez. Comme mentionné par Eero, cela peut arriver si votre élément fait une mise à jour d'Ajax pendant que vous tapez votre texte dans l'élément d'entrée . La solution consiste à envoyer un caractère à la fois et à rechercher à nouveau l'élément d'entrée . (Ex. En rubis illustré ci-dessous)

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

Pour ajouter à la réponse de @ jarib, j'ai créé plusieurs méthodes d'extension qui aident à éliminer la condition de concurrence.

Voici ma configuration:

J'ai une classe appelée "Driver.cs". Il contient une classe statique pleine de méthodes d'extension pour le pilote et d'autres fonctions statiques utiles.

Pour les éléments que j'ai généralement besoin de récupérer, je crée une méthode d'extension comme celle-ci:

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

Cela vous permet de récupérer cet élément de n'importe quelle classe de test avec le code:

driver.SpecificElementToGet();

Maintenant, si cela aboutit à un StaleElementReferenceException, j'ai la méthode statique suivante dans ma classe de pilote:

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

Le premier paramètre de cette fonction est toute fonction qui renvoie un objet IWebElement. Le deuxième paramètre est un timeout en secondes (le code du timeout a été copié à partir de Selenium IDE pour FireFox). Le code peut être utilisé pour éviter l'exception d'élément périmé de la manière suivante:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Le code ci-dessus appellera driver.SpecificElementToGet().Displayedjusqu'à ce qu'il driver.SpecificElementToGet()ne jette aucune exception et .Displayedévalue à trueet 5 secondes ne se sont pas écoulées. Après 5 secondes, le test échouera.

D'un autre côté, pour attendre qu'un élément ne soit pas présent, vous pouvez utiliser la fonction suivante de la même manière:

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

Je pense que j'ai trouvé une approche pratique pour gérer StaleElementReferenceException. Habituellement, vous devez écrire des wrappers pour chaque méthode WebElement pour réessayer des actions, ce qui est frustrant et vous fait perdre beaucoup de temps.

Ajout de ce code

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

avant chaque action WebElement peut augmenter la stabilité de vos tests, mais vous pouvez toujours obtenir StaleElementReferenceException de temps en temps.

Voici donc ce que j'ai trouvé (en utilisant 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;
    }
}

Pour activer cet aspect, créez un fichier src\main\resources\META-INF\aop-ajc.xml et écrivez

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

Ajoutez ceci à votre 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>

Et c'est tout. J'espère que ça aide.


0

Vous pouvez résoudre ce problème en utilisant l'attente explicite afin de ne pas avoir à utiliser l'attente difficile.

Si vous récupérez tous les éléments avec une propriété et que vous l'itérez en utilisant pour chaque boucle, vous pouvez utiliser wait dans la boucle comme ceci,

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
}

ou pour un seul élément, vous pouvez utiliser le code ci-dessous,

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

-1

Dans Java 8, vous pouvez utiliser une méthode très simple pour cela:

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.

Je l'ai ajouté juste maintenant après l'avoir compris. désolé pour le format, c'est la première fois que je poste. J'essaye juste d'aider. Si vous le trouvez utile, partagez-le avec d'autres personnes :)
Alvin Vera

Bienvenue dans stackoverflow! Il est toujours préférable de fournir une brève description d'un exemple de code pour améliorer la précision de la publication :)
Picrofo Software

En exécutant le code ci-dessus, vous risquez de rester indéfiniment dans la boucle, si par exemple il y a une erreur de serveur sur cette page.
grignotez
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.