Reputation: 94
I'm trying to find and click a specific item by matching text from a list of items. The element matched by .list_of_items
is a ul
element contains a list of li>a
element.
I am not sure how to pass the matched element to next function. There is no id or class can use for identifying the element.
driver.isElementPresent(By.css(".list_of_items")).then(function(trueFalse){
if (trueFalse){
return driver.findElements(By.css(".list_of_items a"));
}else{
console.log('err');
}
}).then(function(eleArray){
for (var i=0; i < eleArray.length; i++){
eleArray[i].getInnerHtml().then(function(html){
if (html.trim().toLowerCase() == item_to_search.toLowerCase()){
//
// how to pass the matched element to next function??
//
return true;
}
}).then(function(element){
console.log(element);
}
});
}
});
Upvotes: 3
Views: 4012
Reputation: 151401
Your code currently is doing a case-insensitive search. Note that if you are able to let the search be case-sensitive, then you could find your element with:
browser.findElement(By.linkText(item_to_search)).then(...);
I've written dozens of application tests with Selenium have always been able to search by link text without having to make the search case-sensitive. I would strongly suggest organizing your code so you can do this.
If you cannot, then you'll have to scan each element to find the one you want. Note that it is possible to write an XPath expression that will match text, even case-insensitively but I'm not a fan of XPath when you have to match CSS classes, which you do. So I prefer a method where, like you were doing, you scan your elements. Note that you should use getText()
to test against the text of an element rather than getInnerHtml()
. Testing text values against the HTML is brittle: there may be things appearing in the HTML that do not change what the link text actually says. For instance you could have <a><b>This</b> is a test</a>
The text is This is a test
but if you get the inner HTML of <a>
you get <b>This</b> is a test
, which is not what you want to match against.
Here is an implementation, which also includes an illustration of the By.linkText
method I mentioned earlier:
var webdriver = require('selenium-webdriver');
var By = webdriver.By;
var chrome = require('selenium-webdriver/chrome');
var browser = new chrome.Driver();
browser.get("http://www.example.com");
// Modify the page at example.com to add some test data.
browser.executeScript(function () {
document.body.innerHTML = '\
<ul class="list_of_items">\
<li><a>One</a></li>\
<li><a> Two </a></li>\
<li><a>Three</a></li>\
</ul>';
});
// Illustration of how to get it with By.linkText. This works if you
// are okay with having the test be case-sensitive.
browser.findElement(By.linkText("Two")).getOuterHtml().then(function (html) {
console.log("case-sensitive: " + html);
});
var item_to_search = "TwO"; // Purposely funky case, for illustration.
browser.findElements(By.css(".list_of_items a")).then(function (els){
if (els.length === 0)
// You could put tests here to determine why it is not working.
throw new Error("abort!");
// Convert it once, and only once.
var item_to_search_normalized = item_to_search.toLowerCase();
function check(i) {
if (i >= els.length)
// Element not found!
throw new Error("element not found!");
var el = els[i];
return el.getText().then(function (text) {
if (text.trim().toLowerCase() === item_to_search_normalized)
// Found the element, return it!
return el;
// Element not found yet, check the next item.
return check(i + 1);
});
}
return check(0);
}).then(function (el) {
el.getOuterHtml().then(function (html) {
console.log("case-insensitive: " + html);
});
});
browser.quit();
Additional notes:
Your original code was testing first whether .list_of_items
exists. I did not do this. In general it is better to optimize for the case in which the page is as you expect it. Your code was so that in the case where the page is "good" you always start with a minimum of two operations (check whether .list_of_items
exists, then get the list of anchors). My implementation uses one operation instead. If you want to determine why you get no elements, what you could do is change the throw new Error("abort!")
to perform diagnosis.
Your original code was ending the search as soon as a hit occurred. I've kept this logic intact. So I do not use webdriver.promise.filter
because this will necessarily scan every single element in the list. Even if the very first element is the one you want, all the elements will be visited. This can be very costly because each test will mean one round-trip between your Selenium script and the browser. (Not so bad when everything runs on the same machine. Quite noticeable when you are controlling a browser on a remote server farm.) I'm using recursion to scan the list, which is also what webdriver.promise.filter
does, except that I return early when a hit is found.
This being said, in my code, I'd use executeScript
to reduce the search to one round-trip between my Selenium script and the browser:
browser.executeScript(function () {
// The item_to_search value we passed is accessible as arguments[0];
var item_to_search = arguments[0].toLowerCase();
var els = document.querySelectorAll(".list_of_items a");
for (var i = 0; i < els.length; ++i) {
var el = els[i];
if (el.textContent.trim().toLowerCase() == item_to_search)
return el;
}
return null;
}, item_to_search).then(function (el) {
if (!el)
throw new Error("can't find the element!");
el.getOuterHtml().then(function (html) {
console.log("case-insensitive, using a script: " + html);
});
});
Note that the code in executeScript
is executing in the browser. This is why you have to use arguments
to get what is passed to it and why we have to use .then
to get the return value and show in on the console. (If you put console.log
in the script passed to executeScript
it will show things on the browser's console.)
Upvotes: 1
Reputation: 25034
you can try using the filter
method:
driver.isElementPresent(By.css(".list_of_items")).then(function(trueFalse){
if (trueFalse){
return driver.findElements(By.css(".list_of_items a"));
}else{
console.log('err');
throw new Error('Element not found.'); // Changed, so skips all the next steps till an error handler is found.
}
}).then(function(eleArray){
return webdriver.promise.filter(eleArray, function(element){
return element.getInnerHtml().then(function(innerText){
return innerText.trim().toLowerCase() == item_to_search.toLowerCase();
});
});
}).then(function(reducedElements){
console.log('filtered elements: ', reducedElements.length);
});
Upvotes: 2