¿Cómo evitar "StaleElementReferenceException" en Selenium?

85

Estoy implementando muchas pruebas de Selenium usando Java. A veces, mis pruebas fallan debido a un StaleElementReferenceException. ¿Podría sugerir algunos enfoques para hacer que las pruebas sean más estables?

hoang nguyen
fuente

Respuestas:

89

Esto puede suceder si una operación DOM que ocurre en la página hace que temporalmente el elemento sea inaccesible. Para permitir esos casos, puede intentar acceder al elemento varias veces en un bucle antes de lanzar finalmente una excepción.

Pruebe esta excelente solución 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
fuente
1
¡Guauu! Esto era justo lo que necesitaba. ¡Gracias!
SpartaSixZero
1
También se podría arreglar usando una referencia diferente de elemento.
Ripon Al Wasim
@jspcal, ¡esto funcionó como un encanto para mí! ¡Muchas gracias!
Anthony Okoth
Si lo anterior no lo resuelve, la actualización a la última versión de chromeriver es lo que nos lo resolvió.
Vdex
5
Esto es horrible en muchos sentidos. Resuelve mi problema actual, así que gracias.
Ingeniero de software
66

Tenía este problema de forma intermitente. Sin que yo lo supiera, BackboneJS se estaba ejecutando en la página y reemplazaba el elemento en el que estaba tratando de hacer clic. Mi código se veía así.

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

Lo que, por supuesto, es funcionalmente igual a esto.

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

Lo que ocurría ocasionalmente era que javascript reemplazaría el elemento checkoutLink entre encontrarlo y hacer clic en él, es decir.

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

Lo que condujo legítimamente a una StaleElementReferenceException al intentar hacer clic en el enlace. No pude encontrar ninguna manera confiable de decirle a WebDriver que esperara hasta que el javascript terminara de ejecutarse, así que así es como finalmente lo resolví.

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 intentará continuamente hacer clic en el enlace, ignorando StaleElementReferenceExceptions hasta que el clic sea exitoso o se alcance el tiempo de espera. Me gusta esta solución porque le ahorra tener que escribir cualquier lógica de reintento y usa solo las construcciones integradas de WebDriver.

Kenny
fuente
esta respuesta ha quedado obsoleta.
vaibhavcool20
19

En general, esto se debe a que el DOM se está actualizando y usted está intentando acceder a un elemento nuevo / actualizado, pero el DOM se actualizó, por lo que es una referencia no válida.

Solucione esto primero usando una espera explícita en el elemento para asegurarse de que la actualización esté completa, luego tome una nueva referencia al elemento nuevamente.

Aquí hay un código psuedo para ilustrar (adaptado de un código C # que uso EXACTAMENTE 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 esto ayude!

Jim Holmes
fuente
14

La solución de Kenny es buena, sin embargo, se puede escribir de una manera más elegante.

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

O también:

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

Pero de todos modos, la mejor solución es confiar en la biblioteca Selenide, maneja este tipo de cosas y más. (en lugar de referencias de elementos, maneja proxies para que nunca tenga que lidiar con elementos obsoletos, lo que puede ser bastante difícil). Selenida

cocorossello
fuente
Descargo de responsabilidad: solo soy un usuario feliz de seleniuro, nada que ver con su desarrollo
cocorossello
Su segunda solución funcionaría porque el elemento se vuelve obsoleto cuando hace clic en él, no cuando lo encuentra.
Rajagopalan
Use seleniuro para evitar este problema, mucho más fácil. El selenio no está diseñado para usarse solo debido a este problema y al hecho de que es una API de bajo nivel para un usuario simple
cocorossello
Soy perfectamente consciente de eso. Estoy usando WATIR, que es un envoltorio de Ruby Selenium Binding, WATIR se encarga automáticamente de todos estos problemas (para un elemento obsoleto de instancia). Estoy buscando algo equivalente en el enlace de Java, encontré Selenide pero no sé cómo cambiar la espera implícita y la espera explícita en seleniide. ¿Puede decirme cómo hacer eso? ¿O hay algún material que pueda ofrecerme donde pueda referirme? y ¿cuál es tu opinión sobre FluentLenium?
Rajagopalan
1
La gente debe saber que la respuesta seleccionada por el OP se remonta a 2012. MUCHAS cosas han cambiado en los últimos 7 años. Esta respuesta es más correcta para 2019.
hfontanez
10

La razón por la que StaleElementReferenceExceptionocurre ya se ha establecido: actualizaciones del DOM entre encontrar y hacer algo con el elemento.

Para el problema del clic, he usado recientemente una solución 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();
}

La parte crucial es el "encadenamiento" del propio Selenium a ExpectedConditionstravés del ExpectedConditions.refreshed(). Esto realmente espera y verifica si el elemento en cuestión se ha actualizado durante el tiempo de espera especificado y, además, espera a que se pueda hacer clic en el elemento.

Eche un vistazo a la documentación del método actualizado .

Christian.D
fuente
3

En mi proyecto introduje una noción de StableWebElement. Es un contenedor para WebElement que puede detectar si el elemento está obsoleto y encontrar una nueva referencia al elemento original. He agregado métodos auxiliares para localizar elementos que devuelven StableWebElement en lugar de WebElement y el problema con StaleElementReference desapareció.

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

El código en C # está disponible en la página de mi proyecto, pero podría ser transferido fácilmente a java https://github.com/cezarypiatek/Tellurium/blob/master/Src/MvcPages/SeleniumUtils/StableWebElement.cs

cezarypiatek
fuente
1

Una solución en C # sería:

Clase de ayudante:

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

Invocación:

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

Del mismo modo, se puede utilizar para Java.

George Kargakis
fuente
1

La solución de Kenny está en desuso, use esto, estoy usando la clase de acciones para hacer doble clic, pero puede hacer cualquier cosa.

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
fuente
1

findByAndroidIdMétodo limpio que se maneja con gracia StaleElementReference.

Esto se basa en gran medida en la respuesta de jspcal, pero tuve que modificar esa respuesta para que funcione correctamente con nuestra configuración, por lo que quería agregarla aquí en caso de que sea útil para otros. Si esta respuesta te ayudó, sube la respuesta 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
fuente
0

Esto funciona para mí (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
fuente
0

El problema es que cuando pasa el elemento de Javascript a Java de nuevo a Javascript, puede haber salido del DOM.
Intente hacer todo en Javascript:

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

Prueba esto

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

He encontrado una solución aquí . En mi caso, el elemento se vuelve inaccesible en caso de salir de la ventana, pestaña o página actual y volver de nuevo.

.ignoring (StaleElement ...), .refreshed (...) y elementToBeClicable (...) no ayudaron y estaba obteniendo una excepción en la act.doubleClick(element).build().perform();cadena.

Usando la función en mi clase de prueba principal:

openForm(someXpath);

Mi función 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;
        }
    }

Mi función 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
fuente
0

Podría haber un problema potencial que lleve a la excepción StaleElementReferenceException que nadie mencionó hasta ahora (con respecto a las acciones).

Lo explico en Javascript, pero es lo mismo en Java.

Esto no 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()

Pero instanciar las acciones nuevamente lo 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
fuente
0

Por lo general, StaleElementReferenceException cuando el elemento al que intentamos acceder ha aparecido, pero otros elementos pueden afectar la posición del elemento en el que estamos interesados, por lo tanto, cuando intentamos hacer clic o getText o intentamos realizar alguna acción en WebElement, obtenemos una excepción que generalmente dice que el elemento no está adjunto con DOM .

La solución que probé es la siguiente:

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

En el código anterior, primero trato de esperar y luego hago clic en el elemento si ocurre una excepción, luego lo atrapo y trato de repetirlo, ya que existe la posibilidad de que aún no se carguen todos los elementos y nuevamente se puede producir una excepción.

Rakesh Singh Chouhan
fuente
-4

Tal vez se agregó más recientemente, pero otras respuestas no mencionan la función de espera implícita de Selenium, que hace todo lo anterior por usted y está integrada en Selenium.

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

Esto reintentará las findElement()llamadas hasta que se encuentre el elemento o durante 10 segundos.

Fuente: http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp

Lockedan
fuente
2
Esa solución no evita StaleElementReferenceException
MrSpock
1
Solo para eliminar cualquier confusión sobre las versiones, incluso en la última versión de Selenium implícitamenteWait () NO evita StaleElementReferenceException. Utilizo un método que llama en un bucle con el sueño, hasta el éxito o la cuenta fija.
Angsuman Chakraborty