Archive for January, 2012

Webdriver StaleElementReferenceException

January 26, 2012, 28 Comments

Our end-to-end tests were quite flaky for a while because of a StaleElementReferenceException (*). We have a lot of tests and we run them very often (we have about 180 scenarios and run the tests about 100 times a day). We encountered the “Element is no longer attached to the DOM” exceptions in 5 different cases:

1 - When we didn’t wait (or waited for the wrong thing)
2 - Waiting fails
3 - @FindBy injected elements
4 - findElement on WebElements
5 - Bad luck
I’ll go into details for each case and show how we fixed it.

1 - When we didn’t wait (or for the wrong thing)

This is by far the most common case. Here is an example:
(When you click the “make some attendees optional” in Google Calendar, an icon appears next to each participant.)
  public void clickMakeSomeAttendeesOptional() {
    makeSomeAttendeesOptionalLink.click();
  }
  public void makeOptional(String name) {
    driver.findElement(String.format(REQUIRED_ATTENDEE_ICON, name))
.click(); // StaleElementReferenceException here
  }
Fix: Even though clicking on “make some attendees optional” is very fast in replacing the attendees list and clicking on the required ATTENDEE_ICON works most of the time (and always when you’re trying it out or debugging), that’s not good enough.
The fix is easy, on the action prior to the action that throws the exception, you’ll have to wait.
  public void clickMakeSomeAttendeesOptional() {
    makeSomeAttendeesOptionalLink.click();
    waitUntilVisible(optionalAttendeesLegend);
  }

2 - Waiting fails

[UPDATE: 21.11.2012 (thanks loc) - This isn't needed anymore, because ExpectedConditions handles it now]
This one is a bit more annoying. Every once in a while, the exception gets thrown while waiting for an element to be visible. Example:
WebDriverWait wait = new WebDriverWait(driver, timeout);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(String.format(ATTENDEE, names.get(0)))); // StaleElementReferenceException here
Fix: The reason this happens is that WebDriverWait treats NoSuchElementException different than StaleElementReferenceException. So what we do is wrap the ExpectedCondition with one that throws NoSuchElementException.
// In concrete Page object
  public void waitUntilMonthViewIsDisplayed() {
    waitUntilVisible(By.id("mvEventContainer"));
  }
// In abstract parent
  protected RenderedWebElement waitUntilVisible(By locator) {
    WebDriverWait wait = new WebDriverWait(driver, 20);
    return (RenderedWebElement) wait.until(refreshed(ExpectedConditions.visibilityOfElementLocated(locator)));
  }
  private <T> ExpectedCondition<T> refreshed(final Function<WebDriver, T> originalFunction) {
    return new ExpectedCondition<T>() {
      @Override
      public T apply(WebDriver webdriver) {
        try {
          return originalFunction.apply(webdriver);
        } catch (StaleElementReferenceException sere) {
          throw new NoSuchElementException("Element stale.", sere);
        }
      }
    };
  }

3 - @FindBy injected elements

Now it gets really tricky. In some (rare) occasions the element we get by @FindBy is unusable (i.e. if you use it, it’s stale). This happens even though the page object was only created one line before.
@FindBy(id = "tgCol0")
private RenderedWebElement gridFirstColumn;
  public EventBubble clickGridFirstColumn() {
    gridFirstColumn.click(); // StaleElementReferenceException here
    return newEventBubble();
  }
Fix:
We solve that by wrapping web elements with our own project specific element which knows how to find itself again. We already used our own ElementLocatorFactory for custom @FindBys:
protected Page(WebDriver driver) {
OurElementLocatorFactory factory = new OurElementLocatorFactory(driver);
PageFactory.initElements(factory, this);
}
In OurElementLocator we return the special OurWebElement (or OurRenderedWebElement) that knows how to locate itself again in case of a StaleElementReferenceException:
The methods from WebElement are all overridden in OurWebElement using the same pattern:
  @Override
  public void click() {
    try {
      underlyingElement.click();
    } catch (StaleElementReferenceException sere) {
      againLocate();
      click();
    }
  }
  protected void againLocate() {
    underlyingElement = locator.locate();
  }

4 - findElement on WebElement

It can happen that you use an element you already found and you want to have one of its kids. This may work (i.e. the element is found), but when you want to invoke the method on it, it’s stale.
WebElement element = driver.findElement(...);
WebElement e2 = element.findElement(...);
e2.click();  // StaleElementReferenceException here
Fix: We override the method findElement/findElements method in OurWebElement. If the element is found, we wrap it into a OurWebElement that knows how to locate itself again (see above). If the element is stale, we locate it again and then try again.
class OurWebElement implements WebElement {
public static WebElement wrap(WebElement element, Locator locator) { return new OurWebElement(element, locator); }
  protected OurWebElement(WebElement underlyingElement, Locator locator) {
    this.underlyingElement = underlyingElement;
    this.locator = locator;
  }
  @Override
  public WebElement findElement(By by) {
    try {
      return wrap(underlyingElement.findElement(by),
                  new FindElementLocator(this, by));
    } catch (StaleElementReferenceException sere) {
      againLocate();
     return findElement(by);
    }
    @Override
    public void click() {
      try {
        delay();
        underlyingElement.click();
      } catch (StaleElementReferenceException sere) {
        againLocate();
        click();
      }
    }
}
  protected void againLocate() {
    underlyingElement = locator.locate();
  }

5 - Bad luck

driver.findElement(By.xpath("//div[text()='" + text + "']")).click(); // StaleElementReferenceException here
Yes, this can happen. I know it looks like this should always work but give it enough chances (in javascript-heavy application) and it will fail.
Fix: The page objects aren’t supposed to use WebDriver directly anymore - even though they are passed an instance to WebDriver in the constructor, they are not supposed to keep a reference to it. Instead the parent class provides the functionality:
// In Concrete PageObject class
  public void toggleCalendar(String name) {
    findElement( "//div[@class='t23']//div[text()='" + name + "']").click();
  }
// In Abstract parent class
  protected WebElement findElement(By by) {
    return OurWebElement.wrap(driver.findElement(by), new OurWebElement.FindElementLocator(driver, by));
}

Conclusion


Most of the time the StaleElementReferenceException can be addressed by waiting for something at the right time and this should be your first line of thought. Currently it can be that this is not enough though. Cases 2 - 5 should really be handled by WebDriver IMO. Waiting should always work. Elements provided by @FindBy should always work (when I just created the page object). And the result of my tests should not depend on whether I’m lucky or not :-)

I hope these details can be of help to you when you run into a StaleElementReferenceException: Element is no longer attached to the DOM.
(*) This exception occurs, when you have a reference to an element and you want to call a method on it, but the underlying DOM has changed.