Losowy „element nie jest już dołączony do DOM” StaleElementReferenceException

143

Mam nadzieję, że to tylko ja, ale Selenium Webdriver wydaje się kompletnym koszmarem. Sterownik sieciowy Chrome jest obecnie bezużyteczny, a inne sterowniki są dość zawodne, a przynajmniej tak się wydaje. Walczę z wieloma problemami, ale oto jeden.

Losowo, moje testy zakończą się niepowodzeniem z rozszerzeniem

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

Używam wersji webdriver 2.0b3. Widziałem, jak to się dzieje w przypadku sterowników FF i IE. Jedynym sposobem, w jaki mogę temu zapobiec, jest dodanie rzeczywistego wywołania Thread.sleepprzed wystąpieniem wyjątku. Jest to jednak kiepskie obejście, więc mam nadzieję, że ktoś wskaże błąd z mojej strony, który to wszystko poprawi.

Ray Nicholus
źródło
26
Miejmy nadzieję, że 17 tysięcy wyświetleń wskazuje, że to nie tylko ty;) To chyba najbardziej frustrujący wyjątek selenu.
Mark Mayo
4
48k teraz! Mam ten sam problem ...
Gal.
3
Stwierdzam, że selen to czysty i kompletny śmieć ....
C Johnson,
4
60k, nadal problem :)
Pieter De Bie
w moim przypadku to przez robieniefrom selenium.common.exceptions import NoSuchElementException
kpt. Senkfuss

Odpowiedzi:

119

Tak, jeśli masz problemy ze StaleElementReferenceExceptions, oznacza to, że twoje testy są źle napisane. To stan wyścigu. Rozważ następujący scenariusz:

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

Teraz w miejscu, w którym klikasz element, odwołanie do elementu nie jest już prawidłowe. Jest prawie niemożliwe, aby WebDriver mógł dobrze odgadnąć wszystkie przypadki, w których może się to zdarzyć - więc podnosi ręce i daje kontrolę Tobie, który jako autor testu / aplikacji powinien dokładnie wiedzieć, co może się wydarzyć, a co nie. To, co chcesz zrobić, to wyraźnie poczekać, aż DOM znajdzie się w stanie, w którym wiesz, że nic się nie zmieni. Na przykład użycie WebDriverWait, aby poczekać, aż zaistnieje określony element:

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

Metoda presentOfElementLocated () wyglądałaby mniej więcej tak:

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

Masz rację co do tego, że obecny sterownik Chrome jest dość niestabilny i z przyjemnością usłyszysz, że tułów Selenium ma przepisany sterownik Chrome, w którym większość implementacji została wykonana przez programistów Chromium w ramach ich drzewa.

PS. Alternatywnie, zamiast czekać jawnie, jak w powyższym przykładzie, możesz włączyć niejawne oczekiwanie - w ten sposób WebDriver będzie zawsze zapętlał się do określonego limitu czasu oczekiwania na pojawienie się elementu:

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

Jednak z mojego doświadczenia wynika, że ​​jawne czekanie jest zawsze bardziej niezawodne.

jarib
źródło
2
Czy mam rację mówiąc, że nie można już wczytywać elementów w zmienne i ponownie ich używać? Ponieważ mam ogromną suchą i dynamiczną DSL WATiR, która polega na przepuszczaniu elementów i próbuję przeportować się na webdriver, ale mam ten sam problem. Zasadniczo będę musiał dodać kod, aby ponownie odczytać wszystkie elementy w module dla każdego kroku testowego, który zmienia DOM ...
kinofrost
cześć. Czy mogę zapytać, jakiego typu jest funkcja w tym przykładzie? Nie mogę tego znaleźć .... DZIĘKI!
Hannibal,
1
@Hannibal:, com.google.common.base.Function<F, T>dostarczone przez firmę Guava .
Stephan202
@jarib, mam ten sam problem rok od czasu rozwiązania. problem polega na tym, że piszę swoje skrypty w języku ruby ​​i nie ma funkcji o nazwie „presentOfElementLocated” lub podobnej. Jakieś rekomendacje?
Amey
56
@jarib Nie zgadzam się, to wszystko jest spowodowane źle zaprojektowanym testem. Ponieważ nawet po pojawieniu się elementu po wywołaniu AJAX może nadal działać kod jQuery, który może spowodować wyjątek StaleElementReferenceException. I nie możesz nic zrobić poza dodaniem wyraźnego czekania, co nie wydaje się zbyt miłe. I raczej, że jest to wada projektu w WebDriver
Munch
10

Udało mi się zastosować taką metodę z pewnym sukcesem:

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

Tak, po prostu sonduje element, dopóki nie przestanie być uważany za nieaktualny (świeży?). Naprawdę nie dociera do źródła problemu, ale odkryłem, że WebDriver może być dość wybredny w rzucaniu tego wyjątku - czasami to rozumiem, a czasami nie. Albo może być tak, że DOM naprawdę się zmienia.

Więc nie do końca zgadzam się z powyższą odpowiedzią, że to koniecznie wskazuje na źle napisany test. Mam to na nowych stronach, z którymi w żaden sposób nie wchodziłem w interakcję. Myślę, że jest trochę niestabilności w sposobie reprezentowania DOM lub w tym, co WebDriver uważa za przestarzałe.

aearon
źródło
7
Masz błąd w tym kodzie, nie powinieneś powtarzać rekurencyjnego wywoływania metody bez jakiegoś rodzaju cap, bo rozwalisz swój stos.
Harry
2
Myślę, że lepiej jest dodać licznik lub coś w tym rodzaju, więc gdy wielokrotnie otrzymujemy błąd, możemy faktycznie zgłosić błąd. W przeciwnym razie, jeśli rzeczywiście wystąpi błąd, skończysz w pętli
Sudara
Zgadzam się, że to nie jest wynik źle napisanych testów. Selenium ma tendencję do robienia tego na nowoczesnych stronach internetowych, nawet w przypadku najlepiej napisanych testów - prawdopodobnie dlatego, że witryny internetowe stale odświeżają swoje elementy za pomocą dwukierunkowych powiązań, które są powszechne w reaktywnych strukturach aplikacji internetowych, nawet jeśli nie ma zmian w te elementy trzeba wykonać. Taka metoda powinna być częścią każdego frameworka Selenium, który testuje nowoczesną aplikację internetową.
emery
10

Ten błąd pojawia się czasami, gdy aktualizacje AJAX są w połowie. Kapibara wydaje się być całkiem sprytna, jeśli chodzi o czekanie na zmiany DOM (zobacz Dlaczego wait_until został usunięty z Kapibary ), ale domyślny czas oczekiwania wynoszący 2 sekundy był po prostu niewystarczający w moim przypadku. Zmienione w _spec_helper.rb_ z np

Capybara.default_max_wait_time = 5
Eero
źródło
2
To również rozwiązało mój problem: otrzymywałem StaleElementReferenceError i zwiększenie Capybara.default_max_wait_time rozwiązało problem.
brendan
1

Miałem dzisiaj ten sam problem i stworzyłem klasę opakowania, która sprawdza przed każdą metodą, czy odwołanie do elementu jest nadal prawidłowe. Moje rozwiązanie do odzyskania elementu jest dość proste, więc pomyślałem, że po prostu go udostępnię.

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

Widzisz i „lokalizuję”, a raczej zapisuję element w globalnej zmiennej js i w razie potrzeby pobieram element. Jeśli strona zostanie ponownie załadowana, odniesienie nie będzie już działać. Ale tak długo, jak wprowadzane są tylko zmiany zagłady, pozostaje odniesienie. I to powinno wystarczyć w większości przypadków.

Unika również ponownego przeszukiwania elementu.

Jan

Iwan1993
źródło
1

Miałem ten sam problem i mój był spowodowany starą wersją selenu. Nie mogę zaktualizować do nowszej wersji ze względu na środowisko programistyczne. Problem jest spowodowany przez HTMLUnitWebElement.switchFocusToThisIfNeeded (). Po przejściu do nowej strony może się zdarzyć, że element, który kliknąłeś na starej stronie to oldActiveElement(patrz poniżej). Selen próbuje uzyskać kontekst ze starego elementu i kończy się niepowodzeniem. Dlatego stworzyli próbny haczyk w przyszłych wydaniach.

Kod z wersji sterownika 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();
    }
}

Kod z wersji 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();
}

Bez aktualizacji do wersji 2.23.0 lub nowszej możesz po prostu nadać fokus dowolnemu elementowi. Właśnie użyłem element.click()na przykład.

bandyta-gracz
źródło
1
Wow ... To był naprawdę niejasne znalezisko, dobra robota .. Jestem teraz zastanawiasz się, czy inni kierowcy (np chromedriver) mają podobne problemy zbyt
kevlarr
0

Po prostu zdarzyło mi się, gdy próbowałem wysłać klucze do pola wyszukiwania - które ma automatyczną aktualizację w zależności od tego, co wpisujesz. Jak wspomniała Eero, może się to zdarzyć, jeśli twój element zaktualizuje Ajax podczas wpisywania tekstu wewnątrz elementu wejściowego . Rozwiązaniem jest wysyłanie po jednym znaku na raz i ponowne wyszukiwanie elementu wejściowego . (Np. Rubinem pokazanym poniżej)

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
źródło
0

Aby dodać do odpowiedzi @ jarib, stworzyłem kilka metod rozszerzających, które pomagają wyeliminować stan wyścigu.

Oto moja konfiguracja:

Mam klasę o nazwie „Driver.cs”. Zawiera statyczną klasę pełną metod rozszerzających dla sterownika i innych przydatnych funkcji statycznych.

Dla elementów, które zwykle muszę pobierać, tworzę metodę rozszerzenia, taką jak poniżej:

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

Pozwala to na pobranie tego elementu z dowolnej klasy testowej z kodem:

driver.SpecificElementToGet();

Teraz, jeśli to skutkuje StaleElementReferenceException, mam następującą metodę statyczną w mojej klasie sterownika:

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

Pierwszym parametrem tej funkcji jest dowolna funkcja, która zwraca obiekt IWebElement. Drugi parametr to limit czasu w sekundach (kod limitu czasu został skopiowany z Selenium IDE dla FireFox). Kod może służyć do uniknięcia nieaktualnego wyjątku elementu w następujący sposób:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Powyższy kod będzie wywoływał, driver.SpecificElementToGet().Displayeddopóki driver.SpecificElementToGet()nie zgłosi żadnych wyjątków i .Displayedwylicza do, truea 5 sekund nie minie. Po 5 sekundach test zakończy się niepowodzeniem.

Z drugiej strony, aby poczekać, aż element nie będzie obecny, możesz użyć następującej funkcji w ten sam sposób:

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);
        }
}
Plaża Jared
źródło
0

Myślę, że znalazłem wygodne podejście do obsługi StaleElementReferenceException. Zwykle musisz napisać otoki dla każdej metody WebElement, aby ponowić działania, co jest frustrujące i marnuje dużo czasu.

Dodanie tego kodu

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

przed każdą akcją WebElement może zwiększyć stabilność testów, ale nadal możesz od czasu do czasu uzyskać StaleElementReferenceException.

Oto, co wymyśliłem (używając 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;
    }
}

Aby włączyć ten aspekt, utwórz plik src\main\resources\META-INF\aop-ajc.xml i napisz

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

Dodaj to do swojego 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>

I to wszystko. Mam nadzieję, że to pomoże.

Alexander Oreshin
źródło
0

Możesz rozwiązać ten problem, używając jawnego czekania, aby nie musieć używać twardego czekania.

Jeśli pobierasz wszystkie elementy z jedną właściwością i iterujesz po niej używając dla każdej pętli, możesz użyć wait wewnątrz pętli w ten sposób,

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
}

lub dla pojedynczego elementu możesz użyć poniższego kodu,

new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Your object property"));
driver.findElement("Your object property").click();//or anyother action 
Prajeeth Anand
źródło
-1

W Javie 8 możesz użyć do tego bardzo prostej metody :

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}
Łukasz Jasiński
źródło
-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
źródło
Dodałem go właśnie teraz, po tym jak to rozgryzłem. przepraszam za format, kiedy publikuję po raz pierwszy. Po prostu próbuje pomóc. Jeśli uznasz to za przydatne, podziel się nim z innymi :)
Alvin Vera
Witamy w stackoverflow! Zawsze lepiej jest podać krótki opis przykładowego kodu, aby poprawić dokładność postów :)
Picrofo Software
Uruchamiając powyższy kod, możesz utknąć w pętli na zawsze, jeśli na przykład na tej stronie wystąpił błąd serwera.
chrupać