Como evitar “StaleElementReferenceException” no Selenium?

88

Estou implementando muitos testes Selenium usando Java. Às vezes, meus testes falham devido a a StaleElementReferenceException. Você poderia sugerir algumas abordagens para tornar os testes mais estáveis?

Hoang Nguyen
fonte

Respostas:

90

Isso pode acontecer se uma operação DOM que está acontecendo na página estiver temporariamente deixando o elemento inacessível. Para permitir esses casos, você pode tentar acessar o elemento várias vezes em um loop antes de finalmente lançar uma exceção.

Experimente esta excelente solução de darrelgrainger.blogspot.com :

public boolean retryingFindClick(By by) {
    boolean result = false;
    int attempts = 0;
    while(attempts < 2) {
        try {
            driver.findElement(by).click();
            result = true;
            break;
        } catch(StaleElementException e) {
        }
        attempts++;
    }
    return result;
}
jspcal
fonte
1
Uau! Isso era exatamente o que eu precisava. Obrigado!
SpartaSixZero
1
Ele também poderia ser corrigido usando uma referência de elemento diferente.
Ripon Al Wasim,
@jspcal, funcionou muito bem para mim! Muito obrigado!
Anthony Okoth,
Se isso não resolver o problema, atualizar para o chromedriver mais recente é o que resolveu para nós.
Vdex
5
Isso é horrível de muitas maneiras. Isso resolve meu problema atual, então, obrigado.
Engenheiro de Software
68

Eu estava tendo esse problema de forma intermitente. Sem que eu soubesse, o BackboneJS estava sendo executado na página e substituindo o elemento que eu estava tentando clicar. Meu código era assim.

driver.findElement(By.id("checkoutLink")).click();

Que é funcionalmente igual a isso.

WebElement checkoutLink = driver.findElement(By.id("checkoutLink"));
checkoutLink.click();

O que ocasionalmente acontecia era que o javascript substituía o elemento checkoutLink entre localizá-lo e clicar nele, ou seja,

WebElement checkoutLink = driver.findElement(By.id("checkoutLink"));
// javascript replaces checkoutLink
checkoutLink.click();

O que levou a uma StaleElementReferenceException ao tentar clicar no link. Não consegui encontrar nenhuma maneira confiável de dizer ao WebDriver para esperar até que o javascript terminasse de ser executado, então aqui está como eu resolvi isso.

new WebDriverWait(driver, timeout)
    .ignoring(StaleElementReferenceException.class)
    .until(new Predicate<WebDriver>() {
        @Override
        public boolean apply(@Nullable WebDriver driver) {
            driver.findElement(By.id("checkoutLink")).click();
            return true;
        }
    });

Este código tentará continuamente clicar no link, ignorando StaleElementReferenceExceptions até que o clique seja bem-sucedido ou o tempo limite seja atingido. Eu gosto dessa solução porque ela evita que você tenha que escrever qualquer lógica de repetição e usa apenas as construções integradas do WebDriver.

Kenny
fonte
esta resposta foi descontinuada.
vaibhavcool20
20

Geralmente, isso ocorre porque o DOM está sendo atualizado e você está tentando acessar um elemento atualizado / novo - mas o DOM foi atualizado, portanto é uma referência inválida que você tem.

Contorne isso usando primeiro uma espera explícita no elemento para garantir que a atualização seja concluída e, em seguida, obtenha uma nova referência ao elemento novamente.

Aqui está um código psuedo para ilustrar (adaptado de algum código C # que uso EXATAMENTE para este problema):

WebDriverWait wait = new WebDriverWait(browser, TimeSpan.FromSeconds(10));
IWebElement aRow = browser.FindElement(By.XPath(SOME XPATH HERE);
IWebElement editLink = aRow.FindElement(By.LinkText("Edit"));

//this Click causes an AJAX call
editLink.Click();

//must first wait for the call to complete
wait.Until(ExpectedConditions.ElementExists(By.XPath(SOME XPATH HERE));

//you've lost the reference to the row; you must grab it again.
aRow = browser.FindElement(By.XPath(SOME XPATH HERE);

//now proceed with asserts or other actions.

Espero que isto ajude!

Jim Holmes
fonte
17

A solução de Kenny é boa, mas pode ser escrita de uma forma mais elegante

new WebDriverWait(driver, timeout)
        .ignoring(StaleElementReferenceException.class)
        .until((WebDriver d) -> {
            d.findElement(By.id("checkoutLink")).click();
            return true;
        });

Ou também:

new WebDriverWait(driver, timeout).ignoring(StaleElementReferenceException.class).until(ExpectedConditions.elementToBeClickable(By.id("checkoutLink")));
driver.findElement(By.id("checkoutLink")).click();

Mas de qualquer forma, a melhor solução é contar com a biblioteca Selenide, ela lida com esse tipo de coisas e muito mais. (em vez de referências de elemento, ele lida com proxies para que você nunca tenha que lidar com elementos obsoletos, o que pode ser bastante difícil). Selenide

Cocorossello
fonte
Aviso: Sou apenas um feliz usuário de seleneto, nada a ver com seu desenvolvimento
cocorossello
Sua segunda solução funcionaria porque o elemento fica obsoleto quando você clica nele, e não quando o encontra.
Rajagopalan
Use seleneto para evitar esse problema, muito mais fácil. O Selenium não deve ser usado sozinho devido a este problema e ao fato de ser uma API de baixo nível para um usuário simples
cocorossello
Estou perfeitamente ciente disso. Estou usando WATIR, que é um invólucro em torno de Ruby Selenium Binding, WATIR automaticamente cuida de todos esses problemas (para um elemento obsoleto de instância). Estou procurando algo equivalente na ligação Java, encontrei Selenide, mas não sei como alterar a espera implícita e espera explícita em selenide. Você pode me dizer como fazer isso? Ou há algum material que você possa me oferecer e onde eu possa recomendar? e qual a sua opinião sobre o FluentLenium?
Rajagopalan
1
As pessoas devem saber que a resposta selecionada pelo OP remonta a 2012. MUITAS coisas mudaram nos últimos 7 anos. Esta resposta é mais correta para 2019.
hfontanez
11

A razão pela qual o StaleElementReferenceExceptionocorre já foi definida: atualizações para o DOM entre encontrar e fazer algo com o elemento.

Para o problema do clique, recentemente usei uma solução como esta:

public void clickOn(By locator, WebDriver driver, int timeout)
{
    final WebDriverWait wait = new WebDriverWait(driver, timeout);
    wait.until(ExpectedConditions.refreshed(
        ExpectedConditions.elementToBeClickable(locator)));
    driver.findElement(locator).click();
}

A parte crucial é o "encadeamento" do próprio Selenium ExpectedConditionsatravés do ExpectedConditions.refreshed(). Na verdade, isso espera e verifica se o elemento em questão foi atualizado durante o tempo limite especificado e, adicionalmente, espera que o elemento se torne clicável.

Dê uma olhada na documentação do método atualizado .

Christian.D
fonte
4

Em meu projeto, apresentei uma noção de StableWebElement. É um wrapper para WebElement que é capaz de detectar se o elemento está desatualizado e encontrar uma nova referência para o elemento original. Eu adicionei métodos auxiliares para localizar elementos que retornam StableWebElement em vez de WebElement e o problema com StaleElementReference desapareceu.

public static IStableWebElement FindStableElement(this ISearchContext context, By by)
{
    var element = context.FindElement(by);
    return new StableWebElement(context, element, by, SearchApproachType.First);
} 

O código em C # está disponível na página do meu projeto, mas pode ser facilmente transferido para java https://github.com/cezarypiatek/Tellurium/blob/master/Src/MvcPages/SeleniumUtils/StableWebElement.cs

Cezarypiatek
fonte
1

Uma solução em C # seria:

Aula auxiliar:

internal class DriverHelper
{

    private IWebDriver Driver { get; set; }
    private WebDriverWait Wait { get; set; }

    public DriverHelper(string driverUrl, int timeoutInSeconds)
    {
        Driver = new ChromeDriver();
        Driver.Url = driverUrl;
        Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(timeoutInSeconds));
    }

    internal bool ClickElement(string cssSelector)
    {
        //Find the element
        IWebElement element = Wait.Until(d=>ExpectedConditions.ElementIsVisible(By.CssSelector(cssSelector)))(Driver);
        return Wait.Until(c => ClickElement(element, cssSelector));
    }

    private bool ClickElement(IWebElement element, string cssSelector)
    {
        try
        {
            //Check if element is still included in the dom
            //If the element has changed a the OpenQA.Selenium.StaleElementReferenceException is thrown.
            bool isDisplayed = element.Displayed;

            element.Click();
            return true;
        }
        catch (StaleElementReferenceException)
        {
            //wait until the element is visible again
            element = Wait.Until(d => ExpectedConditions.ElementIsVisible(By.CssSelector(cssSelector)))(Driver);
            return ClickElement(element, cssSelector);
        }
        catch (Exception)
        {
            return false;
        }
    }
}

Invocação:

        DriverHelper driverHelper = new DriverHelper("http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp", 10);
        driverHelper.ClickElement("input[value='csharp']:first-child");

Da mesma forma pode ser usado para Java.

George Kargakis
fonte
1

A solução de Kenny está obsoleta, use isso, estou usando a classe Actions para clicar duas vezes, mas você pode fazer qualquer coisa.

new FluentWait<>(driver).withTimeout(30, TimeUnit.SECONDS).pollingEvery(5, TimeUnit.SECONDS)
                    .ignoring(StaleElementReferenceException.class)
                    .until(new Function() {

                    @Override
                    public Object apply(Object arg0) {
                        WebElement e = driver.findelement(By.xpath(locatorKey));
                        Actions action = new Actions(driver);
                        action.moveToElement(e).doubleClick().perform();
                        return true;
                    }
                });
vaibhavcool20
fonte
1

findByAndroidIdMétodo limpo que lida com elegância StaleElementReference.

Isso é muito baseado na resposta de jspcal, mas tive que modificar essa resposta para que funcionasse perfeitamente com nossa configuração e, portanto, queria adicioná-la aqui, caso seja útil para outras pessoas. Se esta resposta ajudou você, por favor, vote a resposta de jspcal .

// This loops gracefully handles StateElementReference errors and retries up to 10 times. These can occur when an element, like a modal or notification, is no longer available.
export async function findByAndroidId( id, { assert = wd.asserters.isDisplayed, timeout = 10000, interval = 100 } = {} ) {
  MAX_ATTEMPTS = 10;
  let attempt = 0;

  while( attempt < MAX_ATTEMPTS ) {
    try {
      return await this.waitForElementById( `android:id/${ id }`, assert, timeout, interval );
    }
    catch ( error ) {
      if ( error.message.includes( "StaleElementReference" ) )
        attempt++;
      else
        throw error; // Re-throws the error so the test fails as normal if the assertion fails.
    }
  }
}
Joshua Pinter
fonte
0

Isso funciona para mim (100% funcionando) usando C #

public Boolean RetryingFindClick(IWebElement webElement)
    {
        Boolean result = false;
        int attempts = 0;
        while (attempts < 2)
        {
            try
            {
                webElement.Click();
                result = true;
                break;
            }
            catch (StaleElementReferenceException e)
            {
                Logging.Text(e.Message);
            }
            attempts++;
        }
        return result;
    }
Shivam Bharadwaj
fonte
0

O problema é que, no momento em que você passa o elemento de Javascript para Java de volta para Javascript, ele pode ter saído do DOM.
Tente fazer tudo em Javascript:

driver.executeScript("document.querySelector('#my_id').click()") 
pguardiario
fonte
0

Tente isto

while (true) { // loops forever until break
    try { // checks code for exceptions
        WebElement ele=
        (WebElement)wait.until(ExpectedConditions.elementToBeClickable((By.xpath(Xpath))));  
        break; // if no exceptions breaks out of loop
    } 
    catch (org.openqa.selenium.StaleElementReferenceException e1) { 
        Thread.sleep(3000); // you can set your value here maybe 2 secs
        continue; // continues to loop if exception is found
    }
}
CodingGirl1
fonte
0

Eu encontrei solução aqui . No meu caso, o elemento fica inacessível no caso de sair da janela, guia ou página atual e voltar novamente.

.ignoring (StaleElement ...), .refreshed (...) e elementToBeClicable (...) não ajudavam e eu estava recebendo uma exceção na act.doubleClick(element).build().perform();string.

Usando a função na minha classe de teste principal:

openForm(someXpath);

Minha função BaseTest:

int defaultTime = 15;

boolean openForm(String myXpath) throws Exception {
    int count = 0;
    boolean clicked = false;
    while (count < 4 || !clicked) {
        try {
            WebElement element = getWebElClickable(myXpath,defaultTime);
            act.doubleClick(element).build().perform();
            clicked = true;
            print("Element have been clicked!");
            break;
        } catch (StaleElementReferenceException sere) {
            sere.toString();
            print("Trying to recover from: "+sere.getMessage());
            count=count+1;
        }
    }

Minha função BaseClass:

protected WebElement getWebElClickable(String xpath, int waitSeconds) {
        wait = new WebDriverWait(driver, waitSeconds);
        return wait.ignoring(StaleElementReferenceException.class).until(
                ExpectedConditions.refreshed(ExpectedConditions.elementToBeClickable(By.xpath(xpath))));
    }
Gryu
fonte
0

Pode haver um problema potencial que leva à StaleElementReferenceException que ninguém mencionou até agora (em relação às ações).

Eu explico em Javascript, mas é o mesmo em Java.

Isso não vai funcionar:

let actions = driver.actions({ bridge: true })
let a = await driver.findElement(By.css('#a'))
await actions.click(a).perform() // this leads to a DOM change, #b will be removed and added again to the DOM.
let b = await driver.findElement(By.css('#b'))
await actions.click(b).perform()

Mas instanciar as ações novamente vai resolver:

let actions = driver.actions({ bridge: true })
let a = await driver.findElement(By.css('#a'))
await actions.click(a).perform()  // this leads to a DOM change, #b will be removed and added again to the DOM.
actions = driver.actions({ bridge: true }) // new
let b = await driver.findElement(By.css('#b'))
await actions.click(b).perform()
ndsvw
fonte
0

Normalmente StaleElementReferenceException quando o elemento que tentamos acessar apareceu, mas outros elementos podem afetar a posição do elemento em que estamos interessados, portanto, quando tentamos clicar ou obterText ou tentar fazer alguma ação em WebElement, obtemos uma exceção que geralmente diz que o elemento não está anexado ao DOM .

A solução que tentei é a seguinte:

 protected void clickOnElement(By by) {
        try {
            waitForElementToBeClickableBy(by).click();
        } catch (StaleElementReferenceException e) {
            for (int attempts = 1; attempts < 100; attempts++) {
                try {
                    waitFor(500);
                    logger.info("Stale element found retrying:" + attempts);
                    waitForElementToBeClickableBy(by).click();
                    break;
                } catch (StaleElementReferenceException e1) {
                    logger.info("Stale element found retrying:" + attempts);
                }
            }
        }

protected WebElement waitForElementToBeClickableBy(By by) {
        WebDriverWait wait = new WebDriverWait(getDriver(), 10);
        return wait.until(ExpectedConditions.elementToBeClickable(by));
    }

No código acima, primeiro tento esperar e, em seguida, clicar no elemento se ocorrer uma exceção, então eu pego e tento fazer um loop, pois existe a possibilidade de que ainda todos os elementos não possam ser carregados e novamente a exceção pode ocorrer.

Rakesh Singh Chouhan
fonte
-4

Talvez tenha sido adicionado mais recentemente, mas outras respostas deixam de mencionar o recurso de espera implícito do Selenium, que faz tudo o que foi descrito acima para você, e é integrado ao Selenium.

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

Isso vai tentar novamente findElement() chamar até que o elemento seja encontrado, ou por 10 segundos.

Fonte - http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp

Lockan
fonte
2
Essa solução não impede StaleElementReferenceException
MrSpock
1
Apenas para eliminar qualquer confusão nas versões, mesmo na versão mais recente do Selenium implicitlyWait () NÃO evita StaleElementReferenceException. Eu uso um método que chama em um loop com o sono, até o sucesso ou contagem fixa.
Angsuman Chakraborty