3

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);
      }
    });
  }
});
Louis
  • 146,715
  • 28
  • 274
  • 320
Thomas Lo
  • 76
  • 1
  • 6
  • The question is not about how to use javascript but mainly about selenium webdriver jsnode. – Thomas Lo Sep 30 '15 at 09:25
  • Thanks for your comment. I understand how you mean. but the problem i got is not sure how to pass the matched element to next function. – Thomas Lo Oct 01 '15 at 05:58
  • @Louis: This is one of those cases where the answers may be duplicates, but the question is not. There was a discussion on meta about these types, and imho this one should be reopened because the phrasing of the question is very different from the linked to candidate for duplicate, and this one enables others with this problem ("passing of a variable") to find the solution. Answers here can be short, point to the other question for more elaboration, but detail a solution for Thomas. – cfi Oct 01 '15 at 06:46
  • @cfi You wrote your comment after the OP edited the question to no longer make it a duplicate. – Louis Oct 01 '15 at 10:51

2 Answers2

2

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);
});
mido
  • 24,198
  • 15
  • 92
  • 117
1

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:

  1. 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.

  2. 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.)

Louis
  • 146,715
  • 28
  • 274
  • 320
  • Thank you. I took your approach at the end, although I chose mido's as the answer. I changed my code to use css with attribute to search for the desired element which is a lot cleaner in code now. – Thomas Lo Oct 02 '15 at 03:09