it-swarm.com.de

Zufälliges "Element ist nicht mehr an das DOM gebunden" StaleElementReferenceException

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

Zufällig schlagen meine Tests mit einem 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 Web-Treiber-Versionen 2.0b3. Ich habe dies mit FF- und IE -Treibern gesehen. Die einzige Möglichkeit, dies zu verhindern, besteht darin, Thread.sleep einen tatsächlichen Aufruf hinzuzufügen, bevor die Ausnahme auftritt. Dies ist jedoch eine schlechte Lösung, daher hoffe ich, dass jemand auf einen Fehler von meiner Seite hinweisen kann, der das alles noch besser macht.

130
Ray Nicholus

Ja, wenn Sie Probleme mit StaleElementReferenceExceptions haben, weil Ihre Tests schlecht geschrieben wurden. 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 der Stelle, an der Sie auf das Element klicken, ist die Elementreferenz jetzt nicht mehr gültig. Es ist nahezu unmöglich für WebDriver, eine gute Schätzung über alle Fälle zu treffen, in denen dies passieren könnte - so legt der WebDriver seine 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 die Dinge nicht ändern. Verwenden Sie beispielsweise ein WebDriverWait, um zu warten, dass ein bestimmtes Element vorhanden ist:

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

Da der aktuelle Chrome-Treiber ziemlich instabil ist, haben Sie völlig recht, und Sie werden froh sein, dass der Selenium-Trunk einen neu geschriebenen Chrome-Treiber enthält, bei dem die meisten Implementierungen von den Chromium-Entwicklern als Teil ihres Baums durchgeführt wurden.

PS. Alternativ können Sie anstelle des expliziten Wartens wie im obigen Beispiel auch implizite Wartezeiten aktivieren. Auf diese Weise wird der WebDriver immer bis zum angegebenen Timeout durchlaufen, bis das Element vorhanden ist:

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

Nach meiner Erfahrung ist das explizite Warten jedoch immer zuverlässiger. 

114
jarib

Ich erhalte diese Fehlermeldung manchmal, wenn AJAX Updates in der Mitte sind. Capybara scheint ziemlich klug zu sein, auf DOM-Änderungen zu warten (siehe Warum wait_until von Capybara entfernt wurde), aber die Standardwartezeit von 2 Sekunden war in meinem Fall einfach nicht genug. In _spec_helper.rb_ geändert, z.

Capybara.default_wait_time = 5
8
Eero

Ich konnte eine Methode 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, es wird immer nur das Element abgefragt, bis es nicht mehr als veraltet (frisch?) Gilt. Kommt nicht wirklich an der Wurzel des Problems, aber ich habe festgestellt, dass der WebDriver diese Ausnahmebedingung ziemlich wählerisch darstellen kann - manchmal bekomme ich es und manchmal auch nicht. Oder es könnte sein, dass sich das DOM wirklich verä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 bekommen, mit denen ich in keiner Weise interagiert habe. Ich denke, es gibt etwas Flauschigkeit, entweder wie das DOM dargestellt wird oder was WebDriver für abgestanden hält.

8
aearon

Ich hatte das gleiche Problem und meiner wurde durch eine alte Selenium-Version verursacht. Ich kann aufgrund einer 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, die Variable oldActiveElement ist (siehe unten). Selen versucht den Kontext aus dem alten Element abzurufen und schlägt fehl. Aus diesem Grund bauten sie in zukünftigen Versionen einen Try-Catch.

Code aus der Selenium-htmlunit-Treiberversion <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 aus der Selenium-htmlunit-Treiberversion> = 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 jedem Element den Fokus der Seite zuweisen. Ich habe nur element.click() zum Beispiel verwendet.

1
thug-gamer

Ich denke, ich habe einen bequemen Ansatz gefunden, um StaleElementReferenceException zu behandeln. Normalerweise müssen Sie Wrapper für jede WebElement-Methode schreiben, um Aktionen erneut auszuführen, was frustrierend ist und viel Zeit kostet.

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

vorher kann jede WebElement-Aktion die Stabilität Ihrer Tests erhöhen, Sie können jedoch immer noch StaleElementReferenceException abrufen.

Das ist was ich mit AspectJ gefunden habe:

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 die 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 pom.xml hinzu.

<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.

0

Ich hatte heute das gleiche Problem und stellte eine Wrapper-Klasse zusammen, die vor jeder Methode prüft, ob die Elementreferenz noch gültig ist. Meine Lösung, um das Element wiederherzustellen, 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, ich "lokalisieren" oder speichern das Element in einer globalen Js-Variablen und rufen das Element bei Bedarf ab. Wenn die Seite neu geladen wird, funktioniert diese Referenz nicht mehr. Solange jedoch nur Änderungen an Doom vorgenommen werden, bleibt die Referenz erhalten. Und das sollte in den meisten Fällen funktionieren.

Außerdem wird das erneute Durchsuchen des Elements vermieden.

John

0
Iwan1993

Mir ist es gerade beim Versuch passiert, Schlüssel an ein Such-Eingabefeld zu senden - dies hat je nach Eingabe eine automatische Aktualisierung. Wie von Eero erwähnt, kann dies passieren, wenn Ihr Element Ajax-Aktualisierungen vornimmt, während Sie Ihren Text in das Eingabeelement eingeben . Die Lösung ist zu senden Sie jeweils ein Zeichen und suchen Sie erneut nach dem Eingabeelement. (Bsp. In Ruby unten)

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

Um @ jaribs Antwort hinzuzufügen, habe ich verschiedene Erweiterungsmethoden entwickelt, die dazu beitragen, die Race-Bedingung zu beseitigen.

Hier ist mein Setup:

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

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

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

Dadurch können Sie dieses Element mit dem Code aus jeder Testklasse abrufen:

driver.SpecificElementToGet();

Wenn dies zu einer StaleElementReferenceException führt, 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 ein Timeout in Sekunden (der Code für das Timeout wurde aus dem Selenium IDE für FireFox kopiert). Der Code kann verwendet werden, um die Ausnahme des veralteten Elements auf folgende Weise zu vermeiden:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Der obige Code ruft driver.SpecificElementToGet().Displayed auf, bis driver.SpecificElementToGet() keine Ausnahmen ausgibt und .Displayed zu true ausgewertet wird und 5 Sekunden nicht vergangen sind. Nach 5 Sekunden schlägt der Test fehl.

Um zu warten, bis ein Element nicht vorhanden ist, können Sie die folgende Funktion auf dieselbe Weise verwenden:

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