"Element non è più collegato al DOM" StaleElementReferenceException


143

Spero di essere solo io, ma Selenium Webdriver sembra un incubo completo. Il webdriver di Chrome è attualmente inutilizzabile e gli altri driver sono abbastanza inaffidabili, o almeno così sembra. Sto combattendo molti problemi, ma eccone uno qui.

Casualmente, i miei test falliranno con a

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

Sto usando le versioni 2.0b3 di webdriver. Ho visto che ciò accade con i driver FF e IE. L'unico modo per impedirlo è aggiungere una chiamata effettiva Thread.sleepprima che si verifichi l'eccezione. Questa è una pessima soluzione alternativa, quindi spero che qualcuno possa segnalare un errore da parte mia che renderà tutto migliore.


26
Spero che le viste 17k indicano che non sei solo tu;) Questa deve essere l'eccezione più frustrante del selenio là fuori.
Mark Mayo,

4
48k ora! Ho lo stesso problema ...
Gal

3
Sto scoprendo che il selenio è immondizia pura e completa ....
C Johnson,

4
60k, ancora un problema :)
Pieter De Bie,

nel mio caso è stato a causa del farefrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss,

Risposte:


119

Sì, se riscontri problemi con StaleElementReferenceExceptions è perché i tuoi test sono scritti male. È una condizione di gara. Considera il seguente scenario:

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

Ora nel punto in cui si fa clic sull'elemento, il riferimento dell'elemento non è più valido. È quasi impossibile per WebDriver fare una buona ipotesi su tutti i casi in cui ciò potrebbe accadere - quindi alza le mani e ti dà il controllo, che come autore del test / app dovrebbe sapere esattamente cosa può o non può accadere. Quello che vuoi fare è aspettare esplicitamente che il DOM sia in uno stato in cui sai che le cose non cambieranno. Ad esempio, utilizzando un WebDriverWait per attendere l'esistenza di un elemento specifico:

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

Il metodoENCEOfElementLocated () sarebbe simile al seguente:

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

Hai ragione sul fatto che l'attuale driver Chrome sia piuttosto instabile e sarai felice di sapere che il trunk Selenium ha un driver Chrome riscritto, in cui la maggior parte dell'implementazione è stata eseguita dagli sviluppatori Chromium come parte del loro albero.

PS. In alternativa, invece di attendere esplicitamente come nell'esempio sopra, è possibile abilitare le attese implicite - in questo modo WebDriver eseguirà sempre il ciclo fino al timeout specificato in attesa che l'elemento sia presente:

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

Nella mia esperienza, però, l'attesa esplicita è sempre più affidabile.


2
Ho ragione nel dire che non è più possibile leggere elementi in variabili e riutilizzarli? Perché ho un enorme DSL WATiR asciutto e dinamico che si basa sul passaggio di elementi e sto provando a trasferire su webdriver, ma sto avendo lo stesso problema. Fondamentalmente dovrò aggiungere il codice per rileggere tutti gli elementi nel modulo per ogni fase del test che altera il DOM ...
kinofrost

Ciao. Posso chiedere che tipo di funzione è in questo esempio? Non riesco a trovarlo .... GRAZIE!
Annibale,

1
@ Annibale:, com.google.common.base.Function<F, T>fornito da Guava .
Stephan202

@jarib, sto affrontando lo stesso problema un anno dopo la tua soluzione. il problema è che sto scrivendo i miei script in ruby, e non esiste alcuna funzione con il nome di'ENCEOFElementLocated 'o qualcosa di simile. Qualche consiglio?
Amey,

56
@jarib Non sono d'accordo che ciò sia causato da un test mal progettato. Perché anche dopo che l'elemento appare dopo una chiamata AJAX, potrebbe esserci ancora codice jQuery che potrebbe causare StaleElementReferenceException. E non c'è niente che tu possa fare se non aggiungere un'attesa esplicita che non sembra molto piacevole. Penso piuttosto che questo sia un difetto di progettazione in WebDriver
sgranocchiando il

10

Sono stato in grado di utilizzare un metodo come questo con un certo successo:

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

Sì, continua a eseguire il polling dell'elemento fino a quando non viene più considerato stantio (fresco?). Non arriva davvero alla radice del problema, ma ho scoperto che WebDriver può essere piuttosto esigente nel lanciare questa eccezione - a volte capisco, a volte no. O potrebbe essere che il DOM stia davvero cambiando.

Quindi non sono del tutto d'accordo con la risposta sopra che questo indica necessariamente un test scritto male. Ce l'ho su nuove pagine con cui non ho interagito in alcun modo. Penso che ci sia un po 'di debolezza nel modo in cui è rappresentato il DOM o in ciò che WebDriver considera stantio.


7
Hai un bug in questo codice, non dovresti continuare a chiamare il metodo in modo ricorsivo senza una sorta di limite o farai esplodere il tuo stack.
Harry,

2
Penso che sia meglio aggiungere un contatore o qualcosa del genere, quindi quando riceviamo l'errore ripetutamente, possiamo effettivamente lanciare l'errore. Altrimenti se si verifica effettivamente un errore,
finirai

Sono d'accordo che non è il risultato di test scritti male. C'è una tendenza per Selenium a fare questo sui siti Web moderni, anche per i test scritti meglio, probabilmente perché i siti Web aggiornano continuamente i loro elementi tramite i collegamenti bidirezionali che sono comuni nei framework di app Web reattivi, anche quando nessuna modifica a quegli elementi devono essere fatti. Un metodo come questo dovrebbe far parte di ogni framework Selenium che testa una moderna app Web.
smeriglio

10

Ricevo questo errore a volte quando gli aggiornamenti AJAX sono a metà strada. Capybara sembra essere abbastanza intelligente nell'attendere le modifiche al DOM (vedi Perché wait_until è stato rimosso da Capybara ), ma il tempo di attesa predefinito di 2 secondi non era semplicemente abbastanza nel mio caso. Modificato in _spec_helper.rb_ con ad es

Capybara.default_max_wait_time = 5

2
Ciò ha anche risolto il mio problema: stavo ottenendo uno StaleElementReferenceError e aumentando Capybara.default_max_wait_time ho risolto il problema.
brendan,

1

Oggi stavo affrontando lo stesso problema e ho creato una classe wrapper, che controlla prima di ogni metodo se il riferimento dell'elemento è ancora valido. La mia soluzione per recuperare l'elemento è piuttosto semplice, quindi ho pensato di condividerla.

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

Vedete che "localizzo" o piuttosto salvate l'elemento in una variabile js globale e recuperate l'elemento se necessario. Se la pagina viene ricaricata, questo riferimento non funzionerà più. Ma fintanto che vengono apportate solo modifiche al destino, il riferimento rimane. E questo dovrebbe fare il lavoro nella maggior parte dei casi.

Inoltre evita di effettuare nuovamente la ricerca dell'elemento.

John


1

Ho avuto lo stesso problema e il mio è stato causato da una vecchia versione di selenio. Non riesco ad aggiornare a una versione più recente a causa dell'ambiente di sviluppo. Il problema è causato da HTMLUnitWebElement.switchFocusToThisIfNeeded (). Quando si accede a una nuova pagina, può accadere che l'elemento su cui si è fatto clic nella pagina precedente sia oldActiveElement(vedere di seguito). Il selenio cerca di ottenere il contesto dal vecchio elemento e fallisce. Ecco perché hanno realizzato un tentativo di prova nelle versioni future.

Codice dalla versione selenium-htmlunit-driver <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();
    }
}

Codice dalla versione 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();
}

Senza aggiornare alla 2.23.0 o successive, puoi semplicemente dare qualsiasi elemento al focus della pagina. Ho appena usato element.click()per esempio.


1
Wow ... Questa è stata una scoperta davvero oscura, un bel lavoro .. Ora mi chiedo se anche altri driver (es. Chromedriver) abbiano problemi simili
kevlarr

0

Mi è appena successo quando provavo a inviare_chiavi a una casella di input di ricerca - che si aggiorna automaticamente a seconda di ciò che digiti. Come menzionato da Eero, questo può accadere se il tuo elemento esegue un aggiornamento Ajax mentre stai digitando il testo all'interno dell'elemento di input . La soluzione è inviare un carattere alla volta e cercare nuovamente l'elemento di input . (Es. In rubino mostrato sotto)

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

Per aggiungere alla risposta di @ jarib, ho creato diversi metodi di estensione che aiutano a eliminare le condizioni di gara.

Ecco la mia configurazione:

Ho una classe chiamata "Driver.cs". Contiene una classe statica piena di metodi di estensione per il driver e altre utili funzioni statiche.

Per gli elementi che di solito ho bisogno di recuperare, creo un metodo di estensione come il seguente:

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

Ciò consente di recuperare quell'elemento da qualsiasi classe di test con il codice:

driver.SpecificElementToGet();

Ora, se questo si traduce in a StaleElementReferenceException, ho il seguente metodo statico nella mia classe di driver:

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

Il primo parametro di questa funzione è qualsiasi funzione che restituisce un oggetto IWebElement. Il secondo parametro è un timeout in secondi (il codice per il timeout è stato copiato dall'IDE Selenium per FireFox). Il codice può essere utilizzato per evitare l'eccezione dell'elemento non aggiornato nel modo seguente:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Il codice precedente chiamerà driver.SpecificElementToGet().Displayedfino a quando driver.SpecificElementToGet()non genera eccezioni e .Displayedvalutatrue non passerà 5 secondi. Dopo 5 secondi, il test fallirà.

D'altro canto, per attendere che un elemento non sia presente, è possibile utilizzare la seguente funzione allo stesso modo:

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

Penso di aver trovato un approccio conveniente per gestire StaleElementReferenceException. Di solito devi scrivere wrapper per ogni metodo WebElement per ritentare le azioni, il che è frustrante e fa perdere molto tempo.

Aggiunta di questo codice

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

prima che ogni azione di WebElement possa aumentare la stabilità dei test ma è comunque possibile ottenere StaleElementReferenceException di volta in volta.

Quindi questo è quello che mi è venuto in mente (usando 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;
    }
}

Per abilitare questo aspetto creare file src\main\resources\META-INF\aop-ajc.xml e scrivere

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

Aggiungi questo al tuo 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>

E questo è tutto. Spero che sia d'aiuto.


0

Puoi risolverlo usando l'attesa esplicita in modo da non dover usare l'attesa intensa.

Se stai recuperando tutti gli elementi con una proprietà e iterando attraverso di essa usando per ogni ciclo puoi usare wait all'interno del ciclo in questo modo,

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
}

o per singolo elemento puoi usare il codice qui sotto,

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

-1

In Java 8 puoi usare un metodo molto semplice per quello:

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.

L'ho aggiunto subito dopo averlo capito. scusate il formato, questa è la prima volta che scrivo. Sto solo cercando di aiutare. Se lo ritieni utile, condividerlo con gli altri :)
Alvin Vera,

Benvenuto in StackOverflow! È sempre meglio fornire una breve descrizione di un codice di esempio per migliorare l'accuratezza dei post :)
Software Picrofo

L'esecuzione del codice sopra potrebbe rimanere bloccata nel loop per sempre, se ad esempio c'è un errore del server su quella pagina.
munch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.