Aleatório “O elemento não está mais anexado ao DOM” StaleElementReferenceException

143

Espero que seja só eu, mas o Selenium Webdriver parece um pesadelo completo. No momento, o driver da web do Chrome não pode ser usado e os outros drivers não são confiáveis, ao que parece. Estou lutando contra muitos problemas, mas aqui está um.

Aleatoriamente, meus testes falharão com um

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

Estou usando o webdriver versões 2.0b3. Eu já vi isso acontecer com os drivers FF e IE. A única maneira de evitar isso é adicionar uma chamada real Thread.sleepantes que a exceção ocorra. Essa é uma solução alternativa ruim, portanto, espero que alguém possa apontar um erro da minha parte que melhore tudo isso.

Ray Nicholus
fonte
26
Espero que as visualizações de 17k indiquem que não é só você;) Essa deve ser a exceção mais frustrante do Selenium por aí.
Mark Mayo
4
48k agora! Eu tenho o mesmo problema ...
Gal
3
Eu estou achando que o selênio é lixo puro e completo ....
C Johnson
4
60k, ainda um problema :)
Pieter De Bie
no meu caso, foi por causa de fazerfrom selenium.common.exceptions import NoSuchElementException
CPT. Senkfuss 31/01

Respostas:

119

Sim, se você está tendo problemas com o StaleElementReferenceExceptions, é porque seus testes são mal escritos. É uma condição de corrida. Considere o seguinte cenário:

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

Agora, no ponto em que você está clicando no elemento, a referência do elemento não é mais válida. É quase impossível para o WebDriver adivinhar todos os casos em que isso pode acontecer - por isso, ele levanta as mãos e dá controle a você, que, como autor do teste / aplicativo, deve saber exatamente o que pode ou não acontecer. O que você deseja fazer é esperar explicitamente até que o DOM esteja em um estado em que você saiba que as coisas não vão mudar. Por exemplo, usando um WebDriverWait para aguardar a existência de um elemento específico:

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

O método presenceOfElementLocated () ficaria assim:

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

Você está certo sobre o atual driver Chrome ser bastante instável e ficará feliz em saber que o tronco do Selenium possui um driver Chrome reescrito, onde a maior parte da implementação foi feita pelos desenvolvedores do Chromium como parte de sua árvore.

PS. Como alternativa, em vez de esperar explicitamente como no exemplo acima, você pode ativar as esperas implícitas - dessa forma, o WebDriver sempre fará um loop até o tempo limite especificado, aguardando a presença do elemento:

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

Na minha experiência, porém, esperar explicitamente é sempre mais confiável.

jarib
fonte
2
Estou certo ao dizer que não é mais possível ler elementos em variáveis ​​e reutilizá-los? Porque eu tenho um enorme DSL WATiR seco e dinâmico que depende de elementos passantes e estou tentando portar para o webdriver, mas estou tendo o mesmo problema. Essencialmente eu vou ter que adicionar código para re-ler todos os elementos no módulo para cada etapa de teste que altera o DOM ...
kinofrost
Oi. Posso perguntar qual é o tipo Function neste exemplo? Não consigo encontrá-lo .... OBRIGADO!
Hannibal
1
@Hannibal:, com.google.common.base.Function<F, T>fornecido pela Guava .
Stephan202
@jarib, estou enfrentando esse mesmo problema um ano desde a sua solução. o problema é que estou escrevendo meus scripts em ruby ​​e não há função com o nome de 'presenceOfElementLocated' ou algo semelhante. Alguma recomendação?
Amey
56
@jarib Não concordo que tudo isso é causado por testes mal projetados. Porque mesmo depois que o elemento aparece após uma chamada AJAX, ainda pode estar em execução o código jQuery que poderia causar StaleElementReferenceException. E não há nada que você possa fazer, exceto adicionar espera explícita, o que não parece muito bom. Prefiro pensar que esta é uma falha de projeto no WebDriver
Munch
10

Consegui usar um método como este com algum sucesso:

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

Sim, ele continua pesquisando o elemento até que não seja mais considerado obsoleto (novo?). Realmente não chega à raiz do problema, mas descobri que o WebDriver pode ser bastante exigente quanto a lançar essa exceção - às vezes eu entendo e às vezes não. Ou pode ser que o DOM realmente esteja mudando.

Portanto, não concordo totalmente com a resposta acima de que isso indica necessariamente um teste mal escrito. Eu o tenho em páginas novas com as quais não interagi de forma alguma. Eu acho que há alguma falha na forma como o DOM é representado ou no que o WebDriver considera obsoleto.

aearon
fonte
7
Você tem um bug neste código, não deve continuar chamando o método recursivamente sem algum tipo de limite, ou você irá explodir sua pilha.
Harry
2
Eu acho que é melhor adicionar um contador ou algo assim, então, quando estamos recebendo o erro repetidamente, podemos realmente lançá-lo. Caso contrário, se houver realmente um erro, você terminará em loop
Sudara 18/03/15
Concordo que não é o resultado de testes mal escritos. Há uma tendência para o Selenium fazer isso em sites modernos, mesmo para os melhores testes - provavelmente porque os sites estão atualizando continuamente seus elementos por meio de ligações bidirecionais comuns em estruturas de aplicativos da web reativas, mesmo quando não há alterações em esses elementos precisam ser feitos. Um método como esse deve fazer parte de todas as estruturas do Selenium que testam um aplicativo Web moderno.
emery
10

Às vezes, recebo esse erro quando as atualizações do AJAX estão no meio do caminho. A Capivara parece ser bastante esperta em aguardar alterações no DOM (consulte Por que o wait_until foi removido da Capivara ), mas o tempo de espera padrão de 2 segundos simplesmente não foi suficiente no meu caso. Alterado em _spec_helper.rb_ com por exemplo

Capybara.default_max_wait_time = 5
Eero
fonte
2
Isso também corrigiu o meu problema: eu estava recebendo um StaleElementReferenceError e aumentando o Capybara.default_max_wait_time resolvido o problema.
brendan
1

Eu estava enfrentando o mesmo problema hoje e criei uma classe de wrapper, que verifica antes de cada método se a referência do elemento ainda é válida. Minha solução para recuperar o elemento é bastante simples, então eu pensei em compartilhá-lo.

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

Você vê que eu "localizo" ou salve o elemento em uma variável js global e recupere o elemento, se necessário. Se a página for recarregada, essa referência não funcionará mais. Mas desde que apenas sejam feitas alterações na desgraça, a referência permanece. E isso deve fazer o trabalho na maioria dos casos.

Também evita pesquisar novamente o elemento.

John

Iwan1993
fonte
1

Eu tive o mesmo problema e o meu foi causado por uma versão antiga de selênio. Não consigo atualizar para uma versão mais recente devido ao ambiente de desenvolvimento. O problema é causado por HTMLUnitWebElement.switchFocusToThisIfNeeded (). Quando você navega para uma nova página, pode acontecer que o elemento em que você clicou na página antiga seja o oldActiveElement(veja abaixo). O Selenium tenta obter contexto do elemento antigo e falha. É por isso que eles criaram uma tentativa de tentar lançamentos futuros.

Código da versão 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();
    }
}

Código da versão 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();
}

Sem atualizar para a versão 2.23.0 ou mais recente, você pode fornecer qualquer elemento no foco da página. Eu apenas usei element.click()por exemplo.

bandido
fonte
1
Uau ... Este foi um encontrar realmente obscuro, bom trabalho .. Agora estou pensando se os outros condutores (por exemplo chromedriver) têm problemas semelhantes também
kevlarr
0

Aconteceu comigo ao tentar enviar as teclas para uma caixa de entrada de pesquisa - que possui atualização automática dependendo do que você digita. Como mencionado pelo Eero, isso pode acontecer se o seu elemento atualizar o Ajax enquanto você digita o texto dentro do elemento de entrada. . A solução é enviar um caractere de cada vez e procurar novamente o elemento de entrada . (Ex. Em rubi mostrado abaixo)

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

Para adicionar à resposta do @ jarib, criei vários métodos de extensão que ajudam a eliminar a condição de corrida.

Aqui está a minha configuração:

Eu tenho uma classe chamada "Driver.cs". Ele contém uma classe estática cheia de métodos de extensão para o driver e outras funções estáticas úteis.

Para elementos que geralmente preciso recuperar, crio um método de extensão como o seguinte:

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

Isso permite recuperar esse elemento de qualquer classe de teste com o código:

driver.SpecificElementToGet();

Agora, se isso resultar em a StaleElementReferenceException, eu tenho o seguinte método estático na minha classe de 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);
    }
}

O primeiro parâmetro dessa função é qualquer função que retorna um objeto IWebElement. O segundo parâmetro é um tempo limite em segundos (o código do tempo limite foi copiado do Selenium IDE for FireFox). O código pode ser usado para evitar a exceção do elemento obsoleto da seguinte maneira:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

O código acima chamará driver.SpecificElementToGet().Displayedaté que driver.SpecificElementToGet()não haja exceções e .Displayedavalie truee 5 segundos não passaram. Após 5 segundos, o teste falhará.

Por outro lado, para esperar que um elemento não esteja presente, você pode usar a seguinte função da mesma maneira:

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);
        }
}
Praia Jared
fonte
0

Acho que encontrei uma abordagem conveniente para lidar com StaleElementReferenceException. Geralmente, você precisa escrever wrappers para cada método WebElement para tentar novamente as ações, o que é frustrante e gasta muito tempo.

Adicionando este código

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

antes que cada ação do WebElement possa aumentar a estabilidade de seus testes, mas você ainda poderá obter StaleElementReferenceException de tempos em tempos.

Então é isso que eu criei (usando o 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;
    }
}

Para habilitar esse aspecto, crie um arquivo src\main\resources\META-INF\aop-ajc.xml e grave

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

Adicione isto ao seu 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 isso é tudo. Espero que ajude.

Alexander Oreshin
fonte
0

Você pode resolver isso usando a espera explícita para não precisar esperar muito.

Se você buscar todos os elementos com uma propriedade e iterar através dela usando para cada loop, poderá usar wait dentro do loop dessa maneira,

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
}

ou para um único elemento, você pode usar o código abaixo,

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

No Java 8, você pode usar um método muito simples para isso:

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}
Łukasz Jasiński
fonte
-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
fonte
Eu adicionei agora mesmo depois de descobrir. desculpe pelo formato, é a primeira vez que publico. Apenas tentando ajudar. Se você achar que é útil, por favor partilhe-lo aos outros :)
Alvin Vera
Bem-vindo ao stackoverflow! É sempre melhor para fornecer uma breve descrição de um código de exemplo para melhorar a precisão pós :)
Picrofo Software
Ao executar o código acima, você pode ficar preso no loop para sempre, se, por exemplo, houver um erro no servidor nessa página.
Munch #