MattjeS
MattjeS

Reputation: 1397

Selenium Web Driver Stale Reference exception in List of WebElements

I have a list defined as a propterty on a selenium page, e.g:

public IEnumerable<MyItem> MyList =>
        webDriverInstance.FindListElementsWithWait<MyItem>(By.ClassName("my-item"));

Where FindListElementsWithWait is an extension method:

public static IEnumerable<T> FindListElementsWithWait<T>(this IWebDriver driver, By locator, int timeOutMilliseconds = 10000)
{
    var elements = new List<T>();
    var wait = new WebDriverWait(driver, TimeSpan.FromMilliseconds(timeOutMilliseconds));
    wait.Until(d =>
        {
            try
            {  
                elements = d.FindElements(locator).Select(x => (T)Activator.CreateInstance(typeof(T), x)).ToList();
                return true;
            }
            catch (TargetInvocationException)
            {
                return false;
            }
        }
    );

    return elements;
} 

This is making use of WebDriverWait rather than a Thread.Sleep. As this is a list, there is no unique locator for each MyItem. This approach works but there are a few quirks.

for example:

public class MyItem
{
    public string Name { get; private set; }

    public MyItem(IWebElement webElement)
    {
        Name = webElement.FindElement(By.TagName("H4")).Text;
    }
}

This works, but the issue I have is if I had additional properties on MyItem which aren't initially on the element. For example, if I change some state and now a cancel button appears. If I have the cancel button initialisation in the constructor then it would throw a NoSuchElementException and fail on page load. I can get around this by providing a getter body for the new Cancel button property. But this is producing/promoting fragile tests.

Has anyone found a clean way to update/refresh a stale WebElement within a list where there is no unique locator for the stale WebElement?

Upvotes: 2

Views: 1690

Answers (2)

MattjeS
MattjeS

Reputation: 1397

Ok, so this was a race condition in the javascript where the webdriver was doing things before an AJAX request had completed. @JeffC made a good point about using the driver to pull web elements before doing something with them, especially with a list.

In my particular case I was deleting an element from a list. So if you are getting a StaleElementReferenceException and are deleting an element from the view then this maybe useful to you:

  • Ensure you are using the webdriver to get the most up-to-date elements on your page. So if you are changing the state of the page, make sure you are refreshing your elements by locating them again using the webdriver
  • If you are still getting the exception, then it is likely there is a race condition in the javascript. So make sure you are waiting for any animations/ajax request to complete before acting on a webelement reference.

I found the following article quite helpful when solving this problem: https://bocoup.com/weblog/a-day-at-the-races

Upvotes: 1

JeffC
JeffC

Reputation: 25644

In my opinion you are over engineering the problem. I like to keep things simple.

By MyItemsBeforeLocator = By.TagName("h4"); // locator for the element BEFORE the page state change
By MyItemsAfterLocator = By.CssSelector("h4.my-item"); // locator for the element AFTER the page state change
WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
ReadOnlyCollection<IWebElement> Items = wait.Until(ExpectedConditions.VisibilityOfAllElementsLocatedBy(MyItemsBeforeLocator));
// do stuff with Items

// do stuff that changes the state of the page

Items = wait.Until(ExpectedConditions.VisibilityOfAllElementsLocatedBy(MyItemsAfterLocator));
// do stuff with Items

I would suggest you also look into the page object model. Define all your locators for a given page in the page object and then provide methods to perform actions on the page utilizing those locators.

Upvotes: 0

Related Questions