Recording Feature Request: Allow optional custom JS function to calculate correct target CssPath

I have tried a couple of Web based macro recorders and it occurred to me that trying to guess the correct XPath or Css Target for page elements is hard, especially when the page was not built to account for automated testing or when the page is dynamic and it uses ID and Name attributes for its own purposes.

Manually correcting incorrect paths is also very frustrating and time consuming. Doing so takes all the fun out of using a macro recorder and it make the process very error prone.

My suggestion is to allow us to specify a custom in-page JavaScript function (by name) that would do the work of calculating the correct CSS Target based on what we know.

The benefit is that the page developer should be able to create such a function and if they can’t then they are properly incentivized to update their page structure to make things easier.

The Kantu recorder could then pass the page element to this function and the function would be responsible for returning a unique CSS Path.

  • If the function returned a NULL, then Kantu could default to its default options.
  • If the function returned an Error then Kantu could show the error.
  • If the function returned a Path then Kantu could quickly verify that it is valid and that it returns the correct element.

Once a correct function was included on the page, then it should make the process of recording scripts much less painful and even fun.

Thank you very much.

Below I have included an example of such a function. The application I am testing purposely uses ID and Name attributes for its own purposes.

Note: I have included this function as an example only. Other web page structures would necessitate different logic.

==========================================================================

// Generate a unique and short Css Path for an element.
// Does not use the ID or Name attributes.
function getCssPath(inputEl) {
    var curEl = inputEl;
    var elStack = [];
    var lastElsCount = null;  // Used to drop intermediate els that don't add to the specifity...
    var firstClass = function (el) {
        // Purposely only pick on the first class.
        return (el.classList && el.classList.length) ? el.classList[0] : "";
    };
    var elementsFound = function (cssPath) {
        try {
            var elsFound = document.querySelectorAll(cssPath);
            if (elsFound == null || elsFound.length < 1)
                throw "No elements found";
            return elsFound;
        } catch (err) {
            throw "elementsFound: Invalid Path [" + cssPath + "], " + ((typeof err == "string") ? err : err.message);
        }
    };
    var collapsePath = function (cssPath) {
        var pathSplit = cssPath.split('>');
        var newPath = '';
        var joinChar = '';
        for (var i = 0; i < pathSplit.length; i++) {
            if (pathSplit[i] == '')
                joinChar = ' ';
            else {
                newPath += joinChar + pathSplit[i];
                var joinChar = '>';
            }
        }
        return newPath;
    };
    while (curEl.parentNode != null) {
        var sibCount = 0;
        var sibIndex = 0;
        var curClass = firstClass(curEl);

        var curSelector = curEl.nodeName.toLowerCase() + ((curClass) ? '.' + curClass : "");

        // Try this combination.
        var combinedSelector = collapsePath((elStack.length) ? curSelector + ">" + elStack.join('>') : curSelector);
        var elsFound = elementsFound(combinedSelector);
        if (elsFound && elsFound.length == 1 && elsFound[0] === inputEl)
            return combinedSelector;

        // Add the nth-child to see if it is helpful.
        for (var i = 0; i < curEl.parentNode.childNodes.length; i++) {
            var sib = curEl.parentNode.childNodes[i];
            if (sib.nodeName == curEl.nodeName) {
                if (sib === curEl) {
                    sibIndex = sibCount;
                }
                sibCount++;
            }
        }
        if (sibCount > 1) {
            // Note: The 'nth-of-type' is a 1 based array
            curSelector += ':nth-of-type(' + (sibIndex + 1) + ')';

            // Try this combination.
            var combinedSelector = collapsePath((elStack.length) ? curSelector + ">" + elStack.join('>') : curSelector);
            var elsFound = elementsFound(combinedSelector);
            if (elsFound && elsFound.length == 1 && elsFound[0] === inputEl)
                return combinedSelector;
        }
        
        if (lastElsCount == null || lastElsCount > elsFound.length) {
            // If true then we added to the specificity
            lastElsCount = elsFound.length;
        }
        else {
            // The current selector did not help so add a null.
            curSelector = null; 
        }
        elStack.unshift(curSelector);
        curEl = curEl.parentNode;
    }

    // return stack.slice(1); // removes the html element
    var combinedSelector = collapsePath(elStack.slice(1).join('>'));
    if (!combinedSelector) {
        console.error(Error("Can't find Element Path"));
        debugger;
    }
    return combinedSelector;
}

Note: Posted this same suggestion on the Katalon forum.

Yeah, finding the right locator can be very tricky. But there is another solution:
Kantu offers you the option to visually find the right place to click - this way you avoid having to deal with locators, frames and iframes.

I did look at that option and it looks very slick and interesting. I can totally see how it would work for many cases but when I tried it out myself I found that the process of recording the script proved to be a harder then expected.

The problem that I ran into was that the process of moving the focus back and forth changed the appearance of the elements making them difficult to find. I also hope to reuse the script to test a responsive application and the responsive application changes its appearance and layout enough to mess with the image driven lookup.

I very much appreciate that the Kantu XClick and the SeeShell recording is driven by what the user sees rather then the underlying code. That is also important to me so I get what you and doing and see the benefits.

Unfortunately it does not seem to work for me.

A nice new method is relative clicks. It helps to make stable visual anchors, especially if the target area itself is small or (as in your case?) changes colors.

Yeah, if a menu disappears, then any visual method fails :wink:

What can help is the RESIZE command. This makes sure the browser size during replay remains the same. But of course, you might have to use two different macros for desktop size and mobile phone size.

Last but not least, if the web app is reponsive to keystrokes, xtype can help as well.

Hello Ulrich,

Thank you very much for trying to help me with this. In the end I do have to come back to the need for something like what I am suggesting. Some ability on my part to add or point Kantu to an extension method that I would be able to create that would resolve an element to a stable and unique application specific locator.

It would be even better if Kantu could recognize that my application had such a method available and automatically use it.

e.g. If Kantu saw that an application had registered a method named Kantu.getLocator() then it would automatically use it.

If Kantu could automatically recognize such a method, then other developers could recommend Kantu to their users because they would know that their users would now easily be able to create stable Testing or Productivity macros. Kantu would be perfectly positioned to be the recommended tool because it is so easy to download and use. It does not require a massive and intricate install.

At the moment I have my application record the user actions. I then transform those actions into a Kantu macro. That works but it is not easily maintainable from within Kantu.

I suspect that the dev on the Kantu side would not need to be that complex to test this out.

  • If [Kantu.getLocator] is defined then call this method to resolve a locator.
  • If not, then default actions.

I recommend that the Kantu.getLocator() function would be responsible for returning the full locator string. e.g. “id=C000034L”, or “css=table.MyTable”.

Thanks.

Note: I found that Katalon Recorder already had something close to the functionality I requested. And yes, it has really made the process of recording and updating scripts much more enjoyable.

1 Like

I am glad that you found a solution that works for you. Just note that - as far as I can tell - the Katalon recorder project is no longer actively developed* and is not open-source.

(*) There have been no feature updates for a long time, just like with imacros.

Thanks for the heads up. I very much appreciate the open source end.

The feature I used was User_Extension scripts which are also part of Selenium

1 Like

I realize that Kantu is open source but most developers don’t have the extra headspace required to add maintaining a Kantu fork just to add the piece of functionality they need.

That is the nice part about the Selenium User_Extension scripts. We can add only what we need without having to mess with the entire recorder app.

1 Like

as far as I know katalon sources are abailable here: GitHub - katalon-studio/katalon-recorder and the custom scripts for customization are very usefull

katalon sources are abailable here…

Yeah (this is new). But still not published under an Open-Source license. Check their Readme: “Refer to NOTICE and KATALON RECORDER CONTRIBUTION LICENSE AGREEMENT for Katalon Recorder.”