Hello everyone, my name is Denis, I am Software Developer Engineer in Test (SDET) at Bimeister. I am in charge of test software development - frameworks, automated tests, CI Pipelines configuration, and much more.
In this article, I will tell you how we defeated the Stale Element Reference Exception while developing our framework using Selenium WebDriver and C#.
Briefly about SPA
A Single Page Application is a single-page web application in which routing is done on the client side. Instead of sending a request to the server and fetching a new HTML document when the user goes to a new URL, in SPA, the URL is switched programmatically, and the content on the page is rendered dynamically by JavaScript.
While working, the user may feel that he ran not a website, but a desktop application, as it instantly responds to all his actions without noticeable delay. This effect is achieved by using modern web frameworks and libraries: Angular, React, Vue, and others.
SPA has many advantages for the user, but for autotests, it is a bottleneck that causes one of the most frequent issues when using Selenium WebDriver - Stale Element Reference Exception.
Stale Element Reference Exception
The Stale Element Reference Exception is a runtime error. It occurs when test code uses an object and at that time the object's state changes. This can be due to a page refresh, or an event caused by a user interaction that changes the document structure. The modified object remains in the test application's memory, and the link to it is valid, but the object is no longer attached to the page - the reactive application has generated a new DOM element, so invoking the existing reference will result in an exception.
WebDriver throws a stale element reference exception in one of two cases, the first of which is more common than the second:
the element is completely removed - that is, it no longer exists in the DOM;
the element is no longer attached to the DOM - the element exists by its locator, but its reference is stale.
A common reason for an exception is that the JavaScript library deletes an element and replaces it with another one with the same ID or attributes. Although the replacement elements may look identical, they are different. WebDriver does not determine that the replacement elements match the expected ones.
The error occurs precisely when we perform actions on the element when the element has already been found and we are trying to perform some operation on it. For example, to click or enter text:
webElement.Click();
webElement.SendKeys();
Example of Stale Element Reference Exception
Let's look at an abstract example, step by step, to understand the reason for the exception. Suppose we have a folder tree and we want to expand a particular folder in the tree by its name.
To do this, we would write roughly the following algorithm:
Save to a variable a list of all items to be found by the tag for each folder in the tree.
In the list, find an element with the text that matches the name of the desired folder.
Save this element to a variable and call .Click() on the found element to expand the folder.
Let's assume that during this time, the tree has been completely updated because of the implementation of the application. If we try to call .Click() on the found element again, we encounter a Stale ElementReference Exception. Although visually nothing has changed, the scripts have updated the document structure and the element with the desired reference no longer exists in the DOM.
Problem Solution
We can get rid of the Stale Element Reference Exception in several ways:
The easiest way is to add an explicit wait via Thread.Sleep() before finding the element. But this approach doesn't solve the problem completely:
the item reference may still become stale before the action is executed;
since an exception can occur anywhere, we would have to add an explicit wait before each item search, which would negatively affect the performance of the tests without increasing their stability.
A universal way is to handle the Stale Element Reference Exception in a loop with WebElement re-initialization and using the Try Catch block.
Usually, there are two methods with different signatures, because we can execute an action either directly on the anchored page element by its locator, like a button element - or the element found in the loop.
public interface IBrowser
{
void Click(string locator); // clicks on the element found by its locator
void Click(IWebElement element); // clicks on the element
IWebElement WaitWebElement(string locator); // is waiting for an item to appear by tag
}
public class Browser : IBrowser
{
void Click(string locator) { /* function body */ };
void Click(IWebElement element) { /* function body */ };
IWebElement WaitWebElement(string locator) { /* function body */ };
}
A simple situation is when we pass a locator as an argument. In this case, we try to re-find the new element through .FindElement() in the Try-block and the problem with the stale element reference will be defeated:
public void Click(string locator)
{
for (var i = 1; i <= RetryNumber; i++)
{
try
{
Logger.WriteLine($"Try #{i}/{RetryNumber} to click on element with locator: {locator}.");
Driver.FindElement(By.CssSelector(locator)).Click();
Logger.WriteLine($"Clicking on the element: {locator} was successful.");
return;
}
catch (StaleElementReferenceException)
{
Logger.WriteLine($"StaleElementReferenceException was thrown: try #{i}/{RetryNumber}.");
}
}
Logger.WriteLine($"Unable to click on element with locator: {locator}");
throw new TestException(TestErrorMessages.NoSuchElementException);
}
A tricky situation is when IWebElement itself is passed as an argument since there is no way to update the state of the element. There isn't, we checked. In this case, it helps to short-circuit the callback function that the click will be called from:
public void Click(Func<IWebElement> findElement)
{
for (var i = 1; i <= RetryNumber; i++)
{
try
{
Logger.WriteLine($"Try #{i}/{RetryNumber} to click on element.");
findElement().Click();
Logger.WriteLine("Clicking on the element was successful.");
return;
}
catch (StaleElementReferenceException)
{
Logger.WriteLine($"StaleElementReferenceException was thrown: try #{i}/{RetryNumber}.");
}
}
Logger.WriteLine("Failed to click on element.");
throw new TestException(TestErrorMessages.NoSuchElementException);
}
So, we pass a callback function as an argument, which will additionally request an element by tag for us if a Stale Element Reference Exception is caught.
An example of using a method with this signature:
Browser.Click(() => Browser.WaitWebElement("someLocator"));
With this solution, inside Browser.Click(), the .Click() function is executed on the fresh element retrieved through a callback function call. This helps to handle an error where the item reference has time to become stale before the action is executed.
Conclusion
When automating testing with Selenium WebDriver we encounter several problems with modern SPA applications because the DOM element often becomes stale before it is accessed. For example, scripts can update the DOM structure and cause a Stale Element Reference Exception to appear. Implementing the necessary methods to handle such an error will help get rid of flaky tests and increase their stability, which will ultimately reflect the state of your application more reliably.