“Elemento aleatorio ya no está adjunto al DOM” StaleElementReferenceException

Espero que sea solo yo, pero Selenium Webdriver parece una pesadilla completa. El controlador de Chrome actualmente no se puede usar, y los otros controladores son poco confiables, o eso parece. Estoy batallando con muchos problemas, pero aquí hay uno.

Aleatoriamente, mis pruebas fallarán con un

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

Estoy usando las versiones de webdriver 2.0b3. He visto esto suceder con los controladores FF e IE. La única forma en que puedo evitar esto es agregar una llamada real a Thread.sleep antes de que Thread.sleep la excepción. Sin embargo, no es una buena solución, así que espero que alguien pueda señalar un error de mi parte que mejorará todo esto.

Sí, si tiene problemas con StaleElementReferenceExceptions es porque sus pruebas están mal escritas. Es una condición de carrera. Considere la siguiente situación:

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

Ahora, en el punto en que está haciendo clic en el elemento, la referencia del elemento ya no es válida. Es casi imposible para WebDriver adivinar todos los casos en que esto podría suceder, por lo que levanta las manos y le da el control, quien como autor de la prueba / aplicación debe saber exactamente lo que puede suceder o no. Lo que quiere hacer es esperar explícitamente hasta que el DOM esté en un estado en el que sepa que las cosas no cambiarán. Por ejemplo, usar un WebDriverWait para esperar a que exista un 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(); 

El método presenceOfElementLocated () se vería así:

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

Tiene razón en que el controlador Chrome actual es bastante inestable, y le alegrará saber que el troncal de Selenium tiene un controlador Chrome reescrito, donde la mayoría de la implementación fue realizada por los desarrolladores de Chromium como parte de su árbol.

PD. Alternativamente, en lugar de esperar explícitamente como en el ejemplo anterior, puede habilitar esperas implícitas: de esta forma, WebDriver siempre repetirá hasta el tiempo de espera especificado esperando que el elemento se presente:

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

En mi experiencia, sin embargo, esperar explícitamente siempre es más confiable.

Pude utilizar un método como este con cierto éxito:

 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í, solo sigue sondeando el elemento hasta que ya no se considere obsoleto (¿es fresco?). Realmente no llega a la raíz del problema, pero he descubierto que WebDriver puede ser bastante quisquilloso con esta excepción, a veces la obtengo, y otras veces no. O podría ser que el DOM realmente está cambiando.

Por lo tanto, no estoy totalmente de acuerdo con la respuesta anterior de que esto necesariamente indica una prueba mal escrita. Lo tengo en páginas nuevas con las que no he interactuado de ninguna manera. Creo que hay algunas fallas en cómo se representa el DOM, o en lo que WebDriver considera obsoleto.

A veces recibo este error cuando las actualizaciones de AJAX están a mitad de camino. El capibara parece ser bastante listo para esperar los cambios DOM (ver por qué wait_until fue eliminado de Capybara ), pero el tiempo de espera predeterminado de 2 segundos simplemente no fue suficiente en mi caso. Modificado en _spec_helper.rb_ con, por ejemplo,

 Capybara.default_wait_time = 5 

Tuve el mismo problema y el mío fue causado por una versión antigua de selenium. No puedo actualizar a una versión más nueva debido al entorno de desarrollo. El problema está causado por HTMLUnitWebElement.switchFocusToThisIfNeeded (). Cuando navega hacia una nueva página, puede ocurrir que el elemento en el que hizo clic en la página anterior sea el oldActiveElement (vea más abajo). El selenium intenta obtener el contexto del elemento anterior y falla. Es por eso que construyeron una captura de prueba en lanzamientos futuros.

Código de Selenium-htmlunit-driver versión <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 de selenium-htmlunit-driver version> = 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(); } 

Sin actualizar a 2.23.0 o posterior, solo puede dar cualquier elemento en el foco de la página. Acabo de usar element.click() por ejemplo.

Estaba enfrentando el mismo problema hoy e hice una clase contenedora, que verifica antes de cada método si la referencia del elemento sigue siendo válida. Mi solución para recuperar el elemento es bastante simple, así que pensé que simplemente lo compartiría.

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

Verás que “ubico” o mejor guardo el elemento en una variable js global y recupero el elemento si es necesario. Si la página se vuelve a cargar, esta referencia ya no funcionará. Pero mientras solo se hagan cambios a la condena, la referencia permanece. Y eso debería hacer el trabajo en la mayoría de los casos.

También evita re-buscar el elemento.

John

Me acabo de pasar al intentar enviar_keys a un cuadro de entrada de búsqueda, que se ha autoactualizado según lo que escriba. Como lo mencionó Eero, esto puede suceder si su elemento actualiza algunos Ajax mientras está escribiendo su texto dentro del elemento de entrada . La solución es enviar un personaje a la vez y buscar nuevamente el elemento de entrada . (Ej. En ruby ​​se muestra a continuación)

 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 

Para agregar a la respuesta de @ jarib, hice varios métodos de extensión que ayudan a eliminar la condición de carrera.

He aquí mi arreglo:

Tengo una clase llamada “Driver.cs”. Contiene una clase estática llena de métodos de extensión para el controlador y otras funciones estáticas útiles.

Para elementos que comúnmente necesito recuperar, creo un método de extensión como el siguiente:

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

Esto le permite recuperar ese elemento de cualquier clase de prueba con el código:

 driver.SpecificElementToGet(); 

Ahora, si esto da como resultado una StaleElementReferenceException , tengo el siguiente método estático en mi clase de controlador:

 public static void WaitForDisplayed(Func getWebElement, int timeOut) { for (int second = 0; ; second++) { if (second >= timeOut) Assert.Fail("timeout"); try { if (getWebElement().Displayed) break; } catch (Exception) { } Thread.Sleep(1000); } } 

El primer parámetro de esta función es cualquier función que devuelva un objeto IWebElement. El segundo parámetro es un tiempo de espera en segundos (el código del tiempo de espera se copió del Selenium IDE para FireFox). El código se puede usar para evitar la excepción del elemento obsoleto de la siguiente manera:

 MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5); 

El código anterior llamará a driver.SpecificElementToGet().Displayed driver.SpecificElementToGet() hasta que driver.SpecificElementToGet() no arroje excepciones y .Displayed evalúa como true y 5 segundos no han pasado. Después de 5 segundos, la prueba fallará.

Por otro lado, para esperar que un elemento no esté presente, puede usar la siguiente función de la misma manera:

 public static void WaitForNotPresent(Func 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); } } 

Creo que encontré un enfoque conveniente para manejar StaleElementReferenceException. Por lo general, debe escribir wrappers para cada método WebElement para reintentar acciones, lo cual es frustrante y desperdicia mucho tiempo.

Agregar 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 de cada acción de WebElement puede boost la estabilidad de sus pruebas, pero aún puede obtener StaleElementReferenceException de vez en cuando.

Así que esto es lo que se me ocurrió (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; } } 

Para habilitar este aspecto, cree el archivo src\main\resources\META-INF\aop-ajc.xml y escriba

      

Agregue esto a su pom.xml

  1.9.1     org.apache.maven.plugins maven-surefire-plugin 2.22.0   -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"     org.aspectj aspectjweaver ${aspectj.version}     

Y eso es todo. Espero eso ayude.

En Java 8 puedes usar un método muy simple para eso:

 private Object retryUntilAttached(Supplier callable) { try { return callable.get(); } catch (StaleElementReferenceException e) { log.warn("\tTrying once again"); return retryUntilAttached(callable); } } 
 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.