web-dev-qa-db-fra.com

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

J'espère que c'est juste moi, mais Selenium Webdriver semble être un cauchemar complet. Le pilote Web Chrome est actuellement inutilisable et les autres pilotes sont peu fiables, semble-t-il. Je me bats de nombreux problèmes, mais en voici un. 

Au hasard, mes tests échoueront avec 

"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 Webdriver 2.0b3. J'ai vu cela se produire avec les pilotes FF et IE. Le seul moyen d'éviter cela est d'ajouter un appel à Thread.sleep avant que l'exception ne se produise. C'est une mauvaise solution de contournement, alors j'espère que quelqu'un pourra signaler une erreur de ma part qui améliorera la situation.

130
Ray Nicholus

Oui, si vous rencontrez des problèmes avec StaleElementReferenceExceptions, c'est que vos tests sont mal écrits. C'est une condition de concurrence. 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();

Maintenant, au point où vous cliquez sur l'élément, la référence à l'élément n'est plus valide. Il est presque impossible pour WebDriver de deviner avec exactitude tous les cas où cela pourrait se produire - il lâche donc les mains et vous donne le contrôle, qui, en tant qu'auteur du test/de l'application, doit savoir exactement ce qui peut ou ne peut pas se produire. Ce que vous voulez faire, c'est attendre explicitement que le DOM soit dans un état où vous savez que les choses ne changeront pas. Par exemple, en utilisant WebDriverWait pour attendre l’existence d’un élément spécifique:

// 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 de dire que le pilote Chrome actuel est assez instable et vous serez heureux d'apprendre que le coffre de Selenium possède un pilote Chrome réécrit, dans lequel la plupart des implémentations ont été réalisées par les développeurs de Chromium.

PS. Alternativement, au lieu d'attendre explicitement comme dans l'exemple ci-dessus, vous pouvez activer les attentes implicites - de cette façon WebDriver sera toujours bouclé jusqu'à la fin du délai spécifié, en attendant que l'élément soit présent:

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

D'après mon expérience, attendre explicitement est toujours plus fiable. 

114
jarib

Je reçois parfois cette erreur lorsque les mises à jour de AJAX sont à mi-chemin. Capybara semble assez intelligent pour attendre les modifications du DOM (voir Pourquoi wait_until a été supprimé de Capybara ), mais le délai d’attente par défaut de 2 secondes n’était tout simplement pas suffisant dans mon cas. Modifié dans _spec_helper.rb_ avec par exemple.

Capybara.default_wait_time = 5
8
Eero

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 à interroger l'élément jusqu'à ce qu'il ne soit plus considéré comme périmé (frais?). Je ne comprends pas vraiment le problème, mais j’ai constaté que WebDriver peut être assez difficile en ce qui concerne le lancement de cette exception. Parfois, je l’ai, et parfois non. Ou il se peut que le DOM change réellement.

Je ne suis donc pas tout à fait d'accord avec la réponse ci-dessus, à savoir que cela indique nécessairement un test mal écrit. Je l'ai sur de nouvelles pages avec lesquelles je n'ai aucune interaction. Je pense que la manière dont le DOM est représenté ou ce que WebDriver considère comme obsolète présente des faiblesses.

8
aearon

J'ai eu le même problème et le mien a été causé par une ancienne version de Selenium. 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 peut arriver que l'élément sur lequel vous avez cliqué sur l'ancienne page soit la oldActiveElement (voir ci-dessous). Selenium tente d'obtenir le contexte de l'ancien élément et échoue. C'est pourquoi ils ont construit une prise d'essai dans les versions futures.

Code de la version du pilote Selenium-htmlunit-version <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 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);
    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 la version 2.23.0 ou plus récente, vous pouvez simplement donner n'importe quel élément sur la page. Je viens d'utiliser element.click() par exemple.

1
thug-gamer

Je pense avoir trouvé une approche pratique pour gérer StaleElementReferenceException . Généralement, vous devez écrire des wrappers pour chaque méthode WebElement afin de réessayer les actions, ce qui est frustrant et 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, vous pouvez augmenter la stabilité de vos tests, mais vous pouvez toujours obtenir StaleElementReferenceException de temps à autre.

Voici donc ce que je propose (avec 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
Alexander Oreshin

Je faisais face au même problème aujourd'hui et constituais une classe wrapper qui vérifie avant chaque méthode si la référence à l'élément est toujours valide. Ma solution pour retriver l'élément est assez simple et j'ai donc pensé le 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 que je "localise" ou plutôt que vous enregistrez l'élément dans une variable globale js et le récupérez si nécessaire. Si la page est rechargée, cette référence ne fonctionnera plus. Mais tant que seules les modifications sont apportées à Doom, la référence reste. Et cela devrait faire le travail dans la plupart des cas.

En outre, cela évite de rechercher à nouveau l'élément.

John

0
Iwan1993

Cela m’arrive tout juste lorsque j’essaie d’envoyer des clés à une zone de saisie de recherche, qui a une mise à jour automatique en fonction de la saisie. Comme cela a été mentionné par Eero, cela peut se produire si votre élément met à jour une partie de la commande Ajax pendant que vous saisissez votre texte dans l’entrée. . La solution est de envoyer un caractère à la fois et rechercher à nouveau l'élément d'entrée. (Ex. En Ruby 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
ibaralf

Pour ajouter à la réponse de @ jarib, j’ai développé plusieurs méthodes d’extension qui permettent d’éliminer la situation critique.

Voici ma configuration:

J'ai une classe appelée "Driver.cs". Il contient une classe statique complète 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 entraîne une 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 une fonction qui retourne un objet IWebElement. Le deuxième paramètre est un délai d'attente en secondes (le code du délai d'attente a été copié à partir de Selenium IDE pour FireFox). Le code peut être utilisé pour éviter l'exception des éléments périmés de la manière suivante:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Le code ci-dessus appelle driver.SpecificElementToGet().Displayed jusqu'à ce que driver.SpecificElementToGet() ne lève aucune exception et que .Displayed soit évalué à true et que 5 secondes ne se soient 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
Jared Beach