How can I re-write this method to give me a precise xpath or locator? (Avoiding Stale Element Exception)

393 views Asked by At

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.

1

There are 1 answers

8
switch201 On BEST ANSWER

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.