Zufällige StaleElementReferenceException „Element ist nicht mehr an das DOM angehängt“

143

Ich hoffe, es ist nur ich, aber Selenium Webdriver scheint ein Albtraum zu sein. Der Chrome-Webdriver ist derzeit unbrauchbar, und die anderen Treiber sind anscheinend ziemlich unzuverlässig. Ich kämpfe mit vielen Problemen, aber hier ist eines.

Zufällig schlagen meine Tests mit a fehl

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

Ich verwende Webdriver-Versionen 2.0b3. Ich habe dies bei FF- und IE-Treibern gesehen. Die einzige Möglichkeit, dies zu verhindern, besteht darin, einen tatsächlichen Aufruf hinzuzufügen, Thread.sleepbevor die Ausnahme auftritt. Das ist jedoch eine schlechte Problemumgehung, daher hoffe ich, dass jemand auf einen Fehler meinerseits hinweisen kann, der das alles besser macht.

Ray Nicholus
quelle
26
Hoffentlich zeigen die 17.000 Aufrufe, dass es nicht nur Sie sind;) Dies muss die frustrierendste Selenium-Ausnahme da draußen sein.
Mark Mayo
4
48k jetzt! Ich habe das gleiche Problem ...
Gal
3
Ich finde, dass Selen reiner und vollständiger Müll ist ...
C Johnson
4
60k, immer noch ein Problem :)
Pieter De Bie
in meinem Fall war es wegen des Tunsfrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Antworten:

119

Ja, wenn Sie Probleme mit StaleElementReferenceExceptions haben, liegt dies daran, dass Ihre Tests schlecht geschrieben sind. Es ist eine Rennbedingung. Stellen Sie sich das folgende Szenario vor:

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

An dem Punkt, an dem Sie auf das Element klicken, ist die Elementreferenz nicht mehr gültig. Für WebDriver ist es nahezu unmöglich, alle Fälle zu erraten, in denen dies passieren könnte. Daher wirft es die Hände hoch und gibt Ihnen die Kontrolle, wer als Test- / App-Autor genau wissen sollte, was passieren kann oder nicht. Sie möchten explizit warten, bis sich das DOM in einem Zustand befindet, in dem Sie wissen, dass sich nichts ändert. Verwenden Sie beispielsweise ein WebDriverWait, um auf das Vorhandensein eines bestimmten Elements zu warten:

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

Die PresenceOfElementLocated () -Methode würde ungefähr so ​​aussehen:

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

Sie haben völlig Recht damit, dass der aktuelle Chrome-Treiber ziemlich instabil ist, und Sie werden erfreut sein zu hören, dass der Selenium-Trunk über einen neu geschriebenen Chrome-Treiber verfügt, bei dem der größte Teil der Implementierung von den Chromium-Entwicklern als Teil ihres Baums durchgeführt wurde.

PS. Anstatt explizit wie im obigen Beispiel zu warten, können Sie auch implizite Wartezeiten aktivieren. Auf diese Weise wird WebDriver immer bis zum angegebenen Zeitlimit wiederholt, bis das Element vorhanden ist:

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

Nach meiner Erfahrung ist explizites Warten jedoch immer zuverlässiger.

jarib
quelle
2
Habe ich Recht, wenn ich sage, dass es nicht mehr möglich ist, Elemente in Variablen einzulesen und wiederzuverwenden? Weil ich ein riesiges trockenes und dynamisches WATiR-DSL habe, das auf der Übergabe von Elementen beruht, und ich versuche, auf einen Webdriver zu portieren, aber ich habe das gleiche Problem. Im Wesentlichen muss ich Code hinzufügen, um alle Elemente im Modul für jeden
Testschritt
Hallo. Darf ich fragen, welcher Funktionstyp in diesem Beispiel ist? Ich kann es nicht finden ... DANKE!
Hannibal
1
@Hannibal : com.google.common.base.Function<F, T>, bereitgestellt von Guava .
Stephan202
@ Jarib, ich stehe vor dem gleichen Problem ein Jahr seit Ihrer Lösung. Das Problem ist, dass ich meine Skripte in Ruby schreibe und es keine Funktion mit dem Namen 'presentOfElementLocated' oder ähnlichem gibt. Irgendwelche Empfehlungen?
Amey
56
@ Jarib Ich bin nicht der Meinung, dass dies alles durch einen schlecht gestalteten Test verursacht wird. Denn selbst nachdem das Element nach einem AJAX-Aufruf angezeigt wurde, wird möglicherweise noch jQuery-Code ausgeführt, der die StaleElementReferenceException verursachen kann. Und es gibt nichts, was Sie tun können, außer explizites Warten hinzuzufügen, was nicht sehr schön erscheint. Ich denke eher, dass dies ein Designfehler in WebDriver ist
munch
10

Ich konnte eine Methode wie diese mit einigem Erfolg anwenden:

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

Ja, das Element wird nur so lange abgefragt, bis es nicht mehr als abgestanden (frisch?) Betrachtet wird. Kommt dem Problem nicht wirklich auf den Grund, aber ich habe festgestellt, dass der WebDriver beim Auslösen dieser Ausnahme ziemlich wählerisch sein kann - manchmal verstehe ich es und manchmal nicht. Oder es könnte sein, dass sich das DOM wirklich ändert.

Daher stimme ich der obigen Antwort nicht ganz zu, dass dies notwendigerweise auf einen schlecht geschriebenen Test hinweist. Ich habe es auf neuen Seiten, mit denen ich in keiner Weise interagiert habe. Ich denke, es gibt eine gewisse Unschärfe darin, wie das DOM dargestellt wird oder was WebDriver als veraltet ansieht.

aearon
quelle
7
Sie haben einen Fehler in diesem Code. Sie sollten die Methode nicht rekursiv ohne eine Obergrenze aufrufen, da Sie sonst Ihren Stapel sprengen.
Harry
2
Ich denke, es ist besser, einen Zähler oder etwas hinzuzufügen. Wenn wir den Fehler also wiederholt erhalten, können wir ihn tatsächlich auslösen. Andernfalls, wenn es tatsächlich einen Fehler gibt, werden Sie in einer Schleife
enden
Ich bin damit einverstanden, dass es nicht das Ergebnis schlecht geschriebener Tests ist. Selenium tendiert dazu, dies auf modernen Websites zu tun, selbst bei den am besten geschriebenen Tests - wahrscheinlich, weil die Websites ihre Elemente über die in reaktiven Web-App-Frameworks üblichen bidirektionalen Bindungen ständig aktualisieren, auch wenn keine Änderungen an vorgenommen werden Diese Elemente müssen gemacht werden. Eine solche Methode sollte Teil jedes Selenium-Frameworks sein, das eine moderne Web-App testet.
Schmirgel
10

Ich erhalte diesen Fehler manchmal, wenn AJAX-Updates auf halbem Weg sind. Capybara scheint ziemlich schlau zu sein, auf DOM-Änderungen zu warten (siehe Warum wait_until aus Capybara entfernt wurde ), aber die Standardwartezeit von 2 Sekunden war in meinem Fall einfach nicht genug. Geändert in _spec_helper.rb_ mit z

Capybara.default_max_wait_time = 5
Eero
quelle
2
Dies hat auch mein Problem behoben: Ich habe einen StaleElementReferenceError erhalten und durch Erhöhen der Capybara.default_max_wait_time wurde das Problem behoben.
brendan
1

Ich hatte heute das gleiche Problem und habe eine Wrapper-Klasse erstellt, die vor jeder Methode prüft, ob die Elementreferenz noch gültig ist. Meine Lösung, um das Element abzurufen, ist ziemlich einfach, also dachte ich, ich würde es einfach teilen.

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

Sie sehen, dass ich das Element in einer globalen js-Variablen "finde" oder vielmehr speichere und es bei Bedarf abrufe. Wenn die Seite neu geladen wird, funktioniert diese Referenz nicht mehr. Solange jedoch nur Änderungen vorgenommen werden, um das Schicksal zu verhindern, bleibt die Referenz erhalten. Und das sollte in den meisten Fällen funktionieren.

Außerdem wird vermieden, dass das Element erneut durchsucht wird.

John

Iwan1993
quelle
1

Ich hatte das gleiche Problem und mein Problem wurde durch eine alte Selenversion verursacht. Ich kann aufgrund der Entwicklungsumgebung nicht auf eine neuere Version aktualisieren. Das Problem wird durch HTMLUnitWebElement.switchFocusToThisIfNeeded () verursacht. Wenn Sie zu einer neuen Seite navigieren, kann es vorkommen, dass das Element, auf das Sie auf der alten Seite geklickt haben, das ist oldActiveElement(siehe unten). Selen versucht, den Kontext aus dem alten Element zu erhalten und schlägt fehl. Aus diesem Grund haben sie in zukünftigen Versionen einen Try-Catch erstellt.

Code aus der Version <2.23.0 des Selenium-HTML-Einheitentreibers:

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 aus der Version des Selenium-HTML-Einheitentreibers> = 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();
}

Ohne ein Update auf 2.23.0 oder neuer können Sie einfach ein beliebiges Element im Seitenfokus angeben. Ich habe gerade element.click()zum Beispiel verwendet.

Schläger-Spieler
quelle
1
Wow ... Dies war ein wirklich dunkler Fund, gute Arbeit. Ich frage mich jetzt, ob andere Fahrer (z. B. Chromedriver) ähnliche Probleme haben
Kevlarr
0

Das ist mir gerade passiert, als ich versucht habe, Schlüssel in ein Eingabefeld für die Suche zu senden. Je nachdem, was Sie eingeben, wird automatisch aktualisiert. Wie von Eero erwähnt, kann dies passieren, wenn Ihr Element einige Ajax-Aktualisierungen vornimmt, während Sie Ihren Text in das Eingabeelement eingeben . Die Lösung besteht darin , jeweils ein Zeichen zu senden und erneut nach dem Eingabeelement zu suchen . (Bsp. In Rubin unten gezeigt)

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

Um die Antwort von @ jarib zu ergänzen, habe ich verschiedene Erweiterungsmethoden entwickelt, die dazu beitragen, die Rennbedingung zu beseitigen.

Hier ist mein Setup:

Ich habe eine Klasse namens "Driver.cs". Es enthält eine statische Klasse voller Erweiterungsmethoden für den Treiber und andere nützliche statische Funktionen.

Für Elemente, die ich normalerweise abrufen muss, erstelle ich eine Erweiterungsmethode wie die folgende:

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

Auf diese Weise können Sie dieses Element aus einer beliebigen Testklasse mit dem folgenden Code abrufen:

driver.SpecificElementToGet();

Wenn dies zu a führt StaleElementReferenceException, habe ich die folgende statische Methode in meiner Treiberklasse:

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

Der erste Parameter dieser Funktion ist eine Funktion, die ein IWebElement-Objekt zurückgibt. Der zweite Parameter ist eine Zeitüberschreitung in Sekunden (der Code für die Zeitüberschreitung wurde aus der Selenium IDE für FireFox kopiert). Der Code kann verwendet werden, um die veraltete Elementausnahme auf folgende Weise zu vermeiden:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Der obige Code wird aufgerufen, driver.SpecificElementToGet().Displayedbis driver.SpecificElementToGet()keine Ausnahmen .Displayedausgelöst und ausgewertet werden trueund 5 Sekunden nicht vergangen sind. Nach 5 Sekunden schlägt der Test fehl.

Auf der anderen Seite können Sie die folgende Funktion auf die gleiche Weise verwenden, um darauf zu warten, dass ein Element nicht vorhanden ist:

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);
        }
}
Jared Beach
quelle
0

Ich denke, ich habe einen bequemen Ansatz gefunden, um mit StaleElementReferenceException umzugehen. Normalerweise müssen Sie für jede WebElement-Methode Wrapper schreiben, um Aktionen erneut zu versuchen. Dies ist frustrierend und verschwendet viel Zeit.

Diesen Code hinzufügen

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

Bevor jede WebElement-Aktion die Stabilität Ihrer Tests erhöhen kann, können Sie dennoch von Zeit zu Zeit eine StaleElementReferenceException erhalten.

Das habe ich mir also ausgedacht (mit 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;
    }
}

Um diesen Aspekt zu aktivieren, erstellen Sie eine Datei src\main\resources\META-INF\aop-ajc.xml und schreiben Sie

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

Fügen Sie dies Ihrem hinzu 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>

Und das ist alles. Ich hoffe es hilft.

Alexander Oreshin
quelle
0

Sie können dies durch explizites Warten lösen, sodass Sie nicht lange warten müssen.

Wenn Sie alle Elemente mit einer Eigenschaft abrufen und mit jeder Eigenschaft durchlaufen, können Sie wie folgt in der Schleife warten.

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
}

oder für ein einzelnes Element können Sie den folgenden Code verwenden:

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

In Java 8 können Sie dafür eine sehr einfache Methode verwenden :

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}
Łukasz Jasiński
quelle
-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
quelle
Ich habe es gerade hinzugefügt, nachdem ich es herausgefunden hatte. Entschuldigung für das Format, das ich zum ersten Mal poste. Ich versuche nur zu helfen. Wenn Sie es nützlich finden, teilen Sie es bitte mit anderen :)
Alvin Vera
Willkommen bei stackoverflow! Es ist immer besser, eine kurze Beschreibung für einen Beispielcode bereitzustellen, um die Post-Genauigkeit zu verbessern :)
Picrofo Software
Wenn Sie den obigen Code ausführen, bleiben Sie möglicherweise für immer in der Schleife, wenn beispielsweise auf dieser Seite ein Serverfehler vorliegt.
Munch