0

I have been working with GEB and selenium for some time now, and many a time I have run into the dreaded stale element exception because one of the pages I have to test loads dynamically, thus inducing the stale element exception.

I have come very close to creating a catch all solution to the stale element exception but alas not close enough which is why I need help.

My solution was to override the NonEmptyNavigator class that comes with GEB. I am going to show my click() method as an example:

class NonEmptyNavigator extends geb.navigator.NonEmptyNavigator {
    def NonEmptyNavigator() {
        super()
    }


    NonEmptyNavigator(Browser browser, Collection<? extends WebElement> contextElements) {
        super(browser, contextElements)
    }

    //overridden click method (all of the methods are overridden though
    Navigator click(count = 0){
        if (count >= 60) {
            return super.click()
        }
        else{
            try{
                return super.click()
            }
            catch (StaleElementReferenceException s) {
                def oData = this.toString()
                def matcher = Pattern.compile("-> ([^:]+): (.*)]").matcher(oData) //Parses out the xPath
                matcher.find() //Again more Parsing 
                def newXpath = matcher.group(2) //final Parsing step
                newNav = browser.$(By.xpath(newXpath)) //create a new NonEmptyNavigator from the Stale Navigator's xpath 
                return newNav.click(count + 1) //attempt to click the new Navigator
            }
        }
    }
}

Now you might be thinking "Wow this is a really good solution" (and it is) but there are instances where this doesn't work, and I am not sure how to overcome. Let me give an example.

If I do something like this (simplified for readability):

class SomePage extends Page{
    static content = {
        table(required: false) {$(By.xpath("//table/tbody"))}
    }

    //assume this method gets called in a test script
    def someMethod(){
        table.click() //assume this throws a StaleElementException
    }
}

Referencing my overridden method above, oData.toString() ends up being something like: "[[[ChromeDriver: chrome on XP (2cd0a7132456fa2c71d1f798ef32c234)] -> xpath: //table/tbody]]"

as you can see I am able to extract the xpath and create a new navigator object which is great.

Where I run into problems is when faced with a situation like this:

class SomePage extends Page{
    static content = {
        table(required: false) {$(By.xpath("//table/tbody"))}
    }

    //assume this method gets called in a test script
    def someMethod(){
        table.children().getAt(1).children().getAt(2).click() //assume this throws a StaleElementException
    }
}

When executing the click() throws a stale element, oData.toString() appears like this: "[[[[[ChromeDriver: chrome on XP (2cd0a7132456fa2c71d1f798ef32c234)] -> xpath: //table/tbody]] -> xpath: child::*]] -> xpath: child::*]]"

As you can see there is some information showing that I am currently trying to access the child of a child node, but I no longer have the reference I need to redefine that specific element. I don't have the index of the specific child (or children) I want.

I am wondering if there is any way I can obtain that information given my current framework. I would also be open to other ideas and suggestions.

All in all I am essentially looking to create a catch all solution to the StaleElementException. I think I am pretty close and need a little nudge to get over the final hump.

switch201
  • 587
  • 1
  • 4
  • 16
  • Do you actually know what `StaleElementReferenceException` means? I think you shouldn't try to fight this faceless enemy globally :) but try to fix those lines of your code where you might use reference to elements which are no more attached to `DOM` – Andersson Jan 05 '17 at 21:14
  • @Ansersson I new I would get a response like this :), and yes I am well versed in what the StaleElementReferenceException means. As I noted in my description, the page I am testing updates dynamically. In some cases it can update more than once per second even if all the elements remain in their respective locations. This is why I need the solution I am after. I know I could do a try catch over and over on the top level script but that seems messy to me, I want to write something that is more robust. – switch201 Jan 05 '17 at 21:19
  • I'm going to advise against the "catch all solution", too. Have you tried the waitFor clause from Geb? Wouldn't that fix your problems more elegantly? – Alin Pandichi Jan 06 '17 at 11:21
  • @Alin Pandichi I don't think that would work either. Its not a matter of access the element too early. Its a matter of accessing the element too late, because it becomes stale at a very fast rate, (between the navigator declaration and the click() operation) I really wish I had the option to suppress the exception.... I understand why it exists but I Know 100% that I wish to ignore it in this case. – switch201 Jan 06 '17 at 14:45
  • How about this? `new WebDriverWait(driver, timeout) .ignoring(StaleElementReferenceException.class) .until(new Predicate() {` See http://stackoverflow.com/questions/12967541/how-to-avoid-staleelementreferenceexception-in-selenium – Alin Pandichi Jan 06 '17 at 15:02
  • @Alin Pandichi If I am reading this correctly the stale element will be ignored when waiting, but wouldn't the stale element still be thrown on the click? – switch201 Jan 06 '17 at 15:23
  • Like the author of that answer said: `This code will continually try to click the link, ignoring StaleElementReferenceExceptions until either the click succeeds or the timeout is reached.` – Alin Pandichi Jan 06 '17 at 15:41
  • @Alin Padnichi Ok I kinda see it now, but I am not sure how to implement it in my frame work. would I be able to put this code in the Navigator Class so that it can be used globally for all Navigator calls? if so feel free to answer the question showing how this would be done. In that guy's example he has access to the By object used to define the element at his current scope. in my example I do not have access to the By object because once .children() is called I lose it. Does that make sense? – switch201 Jan 06 '17 at 15:47
  • @Alin Padnichi I also think I have an idea of how to answer my question but it will take a bi to implement and test. if it works I will post the answer – switch201 Jan 06 '17 at 15:58
  • @AlinPandichi I figured it out. please see my answer and let me know what you think – switch201 Jan 07 '17 at 18:23
  • @switch201 In my opinion, the "catch all solution" is way too complicated. I would have individually treated each test case where StaleElementReferenceException happens. I can't really give you a solution because I need more context about the test case. I'm glad you found a solution you're satisfied with, though. – Alin Pandichi Jan 08 '17 at 10:21

1 Answers1

0

I was able to figure this out on my own. and now I no longer get the StaleElementReferenceException. I did some more overriding of the NonEmptyNavigator and EmptyNavigator classes. I added in a custom ArrayList field called children. whenever getAt() is called the index of the child being accessed is stored the in the children array. all subsequent calls will pass the children array "down the chain" so that the index can be used when and if the stale element appears. bellow I will show you my code. to save space I only have the click method shown but I did end up overriding a majority of the methods in this class.

class NonEmptyNavigator extends geb.navigator.NonEmptyNavigator {

    public children = [];

    def NonEmptyNavigator() {
        super()
    }


    NonEmptyNavigator(Browser browser, Collection<? extends WebElement> contextElements) {
        super(browser, contextElements)
    }

    def ogClick(){
        ensureContainsSingleElement("click")
        contextElements.first().click()
        this
    }

    NonEmptyNavigator click(count=0) {
        if (count >= 60) {
            return ogClick()
        } else {
            try {
                return ogClick()
            }
            catch (StaleElementReferenceException s) {
                println("Click StaleElement was caught this many times = ${count + 1}")
                def oData = this.toString()
                println("attempting to parse this string's xpath")
                println(oData)
                def matcher = Pattern.compile("-> ([^:]+): (.*)]").matcher(oData);
                matcher.find()
                def orgXpath = matcher.group(2)
                def type = matcher.group(1)
                println("original XPath")
                println(orgXpath)
                def newNav
                def numberOfChildren = StringUtils.countMatches(orgXpath, "-> xpath: child::*")
                if(!(numberOfChildren>0)){
                    try{
                        if (type=="css") {
                            newNav = (NonEmptyNavigator) browser.$(orgXpath)
                            newNav.children.addAll(this.children)
                            return newNav.click(count + 1)

                        } else if (type=="xpath") {
                            newNav = (NonEmptyNavigator) browser.$(By.xpath(orgXpath))
                            newNav.children.addAll(this.children)
                            return newNav.click(count + 1)
                        } else {
                            return ogClick()
                        }
                    }
                    catch(Throwable t){
                        println("Unable to create new navigator from the stale element")
                        return ogClick()
                    }
                }
                else{
                    println("this had a child")
                    println("number of children on record: ${children.size()}")
                    def newXpath = orgXpath.substring(0, orgXpath.indexOf("]]"))
                    children.each{
                        newXpath = newXpath + "/child::*[${it+1}]"
                    }
                    println("New Xpath here")
                    println(newXpath)

                    newNav = browser.$(By.xpath(newXpath))
                    if(!newNav.isEmpty()){
                        newNav = (NonEmptyNavigator) newNav
                    }
                    else{
                        newNav = (EmptyNavigator) newNav
                    }
                    newNav.children.addAll(this.children)
                    return newNav.click(count + 1)
                }
            }
            catch (Throwable t) {
                def loseOfConnection = $(By.xpath("<REDACTED>"))
                def reconnect = $(By.xpath("<REDACTED>"))
                if(loseOfConnection.displayed||reconnect.displayed){
                    println("Loss Of Connection waiting ${count} out of 60 seconds to regain connection")
                    Thread.sleep(1000)
                    return this.click(count+1)
                }
                else{
                    return ogClick()
                }
            }
        }
    }

    NonEmptyNavigator addChild(index){
        println("a child was stored")
        this.children << index
        return this
    }

    NonEmptyNavigator getAt(int index){
        println("getAt was called")
        this.navigatorFor(Collections.singleton(getElement(index))).addChild(index)
    }

    NonEmptyNavigator navigatorFor(Collection<WebElement> contextElements) {
        println("navigateFor was called")
        def answer = browser.navigatorFactory.createFromWebElements(contextElements)
        if(answer.isEmpty()){
            answer = (EmptyNavigator) answer
        }
        else{
            answer = (NonEmptyNavigator) answer
        }
        answer.children.addAll(this.children)
        return answer
    }
}

I believe this is the best way to suppress the StaleElementReferenceException if you wish to do so. Most people will say that the Exception should not be suppressed, but I KNOW for sure that in this case I do not care about the Exception and this is how to stop it from ending your tests. hope you enjoy.

switch201
  • 587
  • 1
  • 4
  • 16
  • How exactly do you use this class? If you really want to make it transparent, i.e. usable from pages and modules just via `$("mySelector")`, I figure you would also have to extend `Page` and `Module`, making sure their `NavigableSupport` instance is initialised with a special `NavigatorFactory` returning your navigator instead of normal ones. This seems to be very complex. But somehow you must make Geb return your overridden `NonEmptyNavigator`. This answer clearly lacks explanation. – kriegaex Feb 22 '17 at 14:03
  • @kriegarex I have been using this class for past few months now since I posted this answer and it has been working very well. It would make alot more sense if GEB just let you suppress the Stale element exception but I digress.... in order to make all Navigator calls reference my new class, I had to add this line to the GebConfig of each Testing Spec:::: `innerNavigatorFactory = { Browser browser, List elements -> elements ? new NonEmptyNavigator(browser, elements) : new EmptyNavigator(browser) }` – switch201 Mar 17 '17 at 20:54
  • Sorry, but this is so contrived. It may work, but I wonder how I write my tests without this being necessary. Still, I want to be open-minded and try your solution on a page where you think it is helpful or even necessary in order to get stable test results. Please point me to a publicly available URL and provide me with a simple page object and Geb test demonstrating your solution. I want to see for myself and compare with what I usually do in similar situations. Then maybe I can use your solution as well or offer a more elegant alternative. What do you say? – kriegaex Mar 19 '17 at 06:35
  • I can not show you the application. and my solution is a bit contrived, yes, but I had other reasons not mentioned in this question for overwriting the navigator methods. I can only give an example. just imagine an app reading many atmospheric values and displaying all that data on a table in real time. Every time one value updates the whole table is updated and goes stale. meanwhile, you have to read the data being displayed on the table with geb. Also, you can randomly lose connection which causes an overlay to appear (Throwable t block), but it goes away if you wait a few seconds. – switch201 Mar 29 '17 at 03:31
  • This can be solved more easily with on-board means. BTW, I meant you to show me something similar on the web, not your internal application. – kriegaex Mar 29 '17 at 09:10
  • If you have a better solution feel free to add another answer. – switch201 Apr 04 '17 at 14:13
  • We are in some kind of deadlock here: I asked you to provide me with a publicly accessible link to a web page (any page, not from your project) featuring content that you would tackle with your solution because you think it is too complex otherwise. Then I could (a) test your solution there (better than just reading code and speculating) and (b) test my solution with the same page. Without a concrete test case it is hard to judge which solution or tool is appropriate. So I am waiting for the link - I want to see your tool in action! Facts over fiction. – kriegaex Apr 04 '17 at 14:53
  • yeah I feel ya. If I am able to find an example I will be sure to share it with you. I am curious what "on-board means" you had in mind. if you don't mind educating me though – switch201 Apr 04 '17 at 21:25