0

I have a super-class Component, inside this class I store common ways to access elements attached to the component.

In the Component constructor, I initElements with the pagefactory

PageFactory.initElements(new AjaxElementLocatorFactory(root,10), this);

As I understand it, this will automatically keep element reference fresh, as long as the elements are annotated by the @FindBy annotation.

I have a specialized Component called SearchResultRow which extends Component.

In this component, I have all the WebElements related to a row in the search result.

What I want to do is to get the first search result and click on it. This is my code for that.

public void clickOnFirstResult() {
    WebElement firstUser = wait.until(ExpectedConditions.elementToBeClickable(searchResultRoot.findElement(By.cssSelector("tbody > tr:nth-child(1)"))));
    new SearchResultRow(firstUser).clickOn(SearchModel.NAME);
}

Here I wait for the element to become clickable, because it's a dynamic element that is not covered by the @FindBy annotation.

SearchModel.NAME refers to a WebElement annotated by @FindBy in the SearchResultRow component. Yet this method sometimes 10-15 % gives the error

stale element reference: element is not attached to the page document

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
santino98
  • 145
  • 1
  • 2
  • 9
  • Does this answer your question? [stale element reference: element is not attached to the page document](https://stackoverflow.com/questions/18225997/stale-element-reference-element-is-not-attached-to-the-page-document) – Prophet Apr 08 '21 at 12:34
  • @Eliyahu Using the methods in that answer seems like really bad practice. I mean they're basically using a work around. – santino98 Apr 08 '21 at 12:39
  • No but this was 7 YEARS ago. Surely we must have gotten a bit further on our journey to flake free selenium tests. – santino98 Apr 08 '21 at 13:02
  • OK, if you have a better way you are welcome to answer there and on other similar posts. This will give you a lot of upvotes. – Prophet Apr 08 '21 at 13:05

2 Answers2

0

It's caused by dynamic DOM. wait.until(ExpectedConditions.elementToBeClickable(searchResultRoot.findElement(By.cssSelector("tbody > tr:nth-child(1)")))); ends when the element is found and clickable. However this occurs during the process of DOM building, so when you trying to click on this element it may disappear / change so the element you are referencing to is no more exists.
The simplest way to work with dynamic DOM pages is each time the DOM / page is reloaded / rebuilt is to give some enough delay time and only after that to deal with it's elements.
You already asked similar question and I already answered you there.

Prophet
  • 32,350
  • 22
  • 54
  • 79
  • "The simplest way to work with dynamic DOM pages is each time the DOM / page is reloaded / rebuilt is to give some enough delay time and only after that to deal with it's elements." What's a good practice for that? That is what i was trying to achieve by using the AjaxElementLocatorFactory – santino98 Apr 08 '21 at 12:34
  • I'd advise you using a loop. Try clicking on the element. If succeed - OK, leave the loop, if failed and the Exception is thrown increase some counter in catch section, wait some short time and try again. – Prophet Apr 08 '21 at 12:38
0

Root cause

Stale element reference refers to web elements that were obtained before a page refresh but attempted to be used (i.e. calling click() method after said refresh.)

Solution

Wait.... Which are the best ways to wait for page reloads? One way is to use Implicit Waits. One very common way to implicitly wait some amount of time:

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

This is how it is done in Selenium Java, but there should be an equivalent method for whichever version of Selenium you are using. The question now is, what does this do? This is the way to tell the web driver to wait the given amount of time BEFORE attempting to obtain a web element. When the implicit wait time is set, it will remain set for the duration of your test session. Therefore, this is typically done at the very beginning before any tests begin to execute. More specifically, you should do it immediately after getting the web driver instance.

That said, this should not be enough. For example, what if the component become stale after initial load of a page? For that, you should use ExpectedConditions class and set an explicit wait for some condition to occur:

WebDriverWait wait = new WebDriverWait(driver, 10); // timeout of 10 seconds and polling the default value of 500 ms, or...
WebDriverWait wait = new WebDriverWait(driver, 10, 100); // timeout of 10 seconds and polling every 100 ms

Then use wait object to set your expected condition and obtain the required web element

WebElement element = wait.until(...);

You will need to pass the appropriate ExpectedConditions to the above method. Common ones to be used to avoid staleness:

WebElement element = wait.until(ExpectedConditions.elementSelectionStateToBe(...));
WebElement element = wait.until(ExpectedConditions.refreshed(...));

Lastly, you will check to see what the state of the element is before interacting with it. For example:

WebElement newElement = wait.until(ExpectedConditions.elementToBeClickable(element));
newElement.click();

In this case, element and newElement are the same object so using just wait.until(ExpectedConditions.elementToBeClickable(element)); should be enough before calling element.click(). By this point, you have established that the element is not stale and it is visible and enabled; and therefore, safe to be clicked (for instance).

hfontanez
  • 5,774
  • 2
  • 25
  • 37
  • Isen't this what i'm already doing with `WebElement firstUser = wait.until(ExpectedConditions.elementToBeClickable(searchResultRoot.findElement(By.cssSelector("tbody > tr:nth-child(1)"))));` – santino98 Apr 08 '21 at 14:27
  • @santino98 it is not a matter of WHAT you are doing. It is a matter of WHEN you are doing it. If you set your condition BEFORE the page refreshes, you will run into stale element problems. Remember, stale kind of means "old". Meaning that there is a newer element in the DOM than the one you have. – hfontanez Apr 08 '21 at 14:29
  • 1
    that's a good point. I think what might be happening is that angularJS is messing up the DOM while i'm trying to wait for my condition. the condition is true but angular is not done. – santino98 Apr 08 '21 at 15:20
  • @santino98 without knowing your specific case, that would be my guess. I have experienced similar issues as well. The basic recipe is 1) set a default time (implicit wait) for page loads to complete to block obtaining elements until that time elapses, 2) use `ExpectedConditions` to set your specific condition to wait for before interacting with web element, and 3) interact with element. This will work for 80%-90% of cases and for remaining cases you'll have to tweak this approach slightly. – hfontanez Apr 08 '21 at 15:25
  • @santino98 one thing I have done in some cases is to catch the exception (i.e. stale element) and on the catch block retry the process. Then, only fail after the retry. – hfontanez Apr 08 '21 at 15:26