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?
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?
Respuestas:
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;
}
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.
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!
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
La razón por la que StaleElementReferenceException
ocurre 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 ExpectedConditions
travé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 .
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
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.
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;
}
});
findByAndroidId
Mé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.
}
}
}
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;
}
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()")
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
}
}
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))));
}
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()
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.
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