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?
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?
Respostas:
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;
}
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.
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!
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
A razão pela qual o StaleElementReferenceException
ocorre 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 ExpectedConditions
atravé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 .
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
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.
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;
}
});
findByAndroidId
Mé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.
}
}
}
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;
}
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()")
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
}
}
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))));
}
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()
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.
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