2

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.

  • It is relying on the constructor of MyItem (T in the extension method example) to throw an exception (StaleElementReferenceException)
  • this means putting property initialisation in the constructor for MyItem

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?

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

0

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.

4 Comments

Thanks for your response, to avoid StaleElementReferenceException do the before and after locators have to be different? I've tried an approach similar to your answer but still getting the exception: Driver.Instance.FindElementsWithWait(By.CssSelector(".my-item h4")).Select(we => we.Text).ToList(); This is throwing the exception still. The FindElementsWithWait encapsulates the wait code in your answer
No, the locators should be specific to what ever state they are in at the time you need to grab them. If they are always the same, then you should only need one locator, you just have to go back and scrape it again.
Yes, your custom function encapsulates the wait code in my answer but that's the point... you had to write and debug 15 lines of code that does the same thing as an out-of-the-box one-line method. IMO, it's more work, it's more likely to have bugs, and you have to maintain it... why not use the out-of-box-method?
the function is: public static IEnumerable<IWebElement> FindElementsWithWait(this IWebDriver driver, By locator, int timeoutMilliseconds = 10000) { var wait = new WebDriverWait(driver, TimeSpan.FromMilliseconds(timeoutMilliseconds)); return wait.Until(ExpectedConditions.VisibilityOfAllElementsLocatedBy(locator)); } it's not 15 lines of code and is reusable wherever I want to use FindElements with a wait. My guess is that there is a race condition with the javascript and the webdriver wait.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.