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 macro 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 the recorder could use its default options.
- If the function returned an Error then the recorder could show the error.
- If the function returned a Path then the recorder 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: I have posted this same suggestion to the Kantu forum.