18

We have a menu represented as a ul->li list (simplified):

<ul class="dropdown-menu" role="menu">
    <li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
        ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
        <a href="" class="ng-binding"> Menu Item 1</a>
    </li>
    ...
    <li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
        ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
        <a href="" class="ng-binding"> Menu Item 2</a>
    </li>
</ul>

Where somewhere at position N, there is a divider, which can be identified by evaluating filterItem.isDivider or by checking the text of the a link (in case of a divider, it's empty).

Now, the goal is to get all of the menu items that are located before the divider. How would you approach the problem?


My current approach is rather generic - to extend ElementArrayFinder and add takewhile() function (inspired by Python's itertools.takewhile()). Here is how I've implemented it (based on filter()):

protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
    var self = this;
    var getWebElements = function() {
        return self.getWebElements().then(function(parentWebElements) {
            var list = [];
            parentWebElements.forEach(function(parentWebElement, index) {
                var elementFinder =
                    protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);

                list.push(whileFn(elementFinder, index));
            });
            return protractor.promise.all(list).then(function(resolvedList) {
                var filteredElementList = [];
                for (var index = 0; index < resolvedList.length; index++) {
                    if (!resolvedList[index]) {
                        break;
                    }
                    filteredElementList.push(parentWebElements[index])
                }
                return filteredElementList;
            });
        });
    };
    return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};

And, here is how I'm using it:

this.getInclusionFilters = function () {
    return element.all(by.css("ul.dropdown-menu li")).takewhile(function (inclusionFilter) {
        return inclusionFilter.evaluate("!filterItem.isDivider");
    });
};

But, the test is just hanging until jasmine.DEFAULT_TIMEOUT_INTERVAL is reached on the takewhile() call.

If I put console.logs into the loop and after, I can see that it correctly pushes the elements before the divider and stops when it reaches it. I might be missing something here.

Using protractor 2.2.0.


Also, let me know if I'm overcomplicating the problem.

alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195

2 Answers2

7

Maybe I'm missing something, but couldn't you just go through ul li a elements while they gave you something from getText(), and store them to some array, or do something with them directly in that loop?

var i = 0;
var el = element.all(by.css('ul li a'));
var tableItems = [];
(function loop() {
    el.get(i).getText().then(function(text){
        if(text){
            tableItems.push(el.get(i));
            i+=1;
            loop();
        }
    });
}());
cvakiitho
  • 1,377
  • 13
  • 27
  • 1
    Thank you for the option! What I'm not satisfied is that, in this case, as a result I'll get an array of elements and not an ElementArrayFinder which makes it inconvenient to work with later - for instance, I won't be able to call `getText()` on it and get an array of texts, or use `map()`, `each()` or other functional syntax available in protractor..but, in general, it gets the job done, I agree. – alecxe Sep 16 '15 at 04:05
  • 1
    Given that now I can [make an `ElementArrayFinder` instance out of an array of element finders](http://stackoverflow.com/questions/32599679/making-elementarrayfinder-from-an-array-of-elementfinders), this option becomes the way-to-go for me. Thanks again! – alecxe Sep 23 '15 at 13:23
3

takewhile() actually worked for me once I removed the protractor.promise = require("q"); from onPrepare() - this was there to replace protractor.promise with q on the fly to be able to use the syntactic sugar like spread() function. Apparently, it is not safe to use q in place of protractor.promise.

All I have to do now is to add this to onPrepare():

protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
    var self = this;
    var getWebElements = function() {
        return self.getWebElements().then(function(parentWebElements) {
            var list = [];
            parentWebElements.forEach(function(parentWebElement, index) {
                var elementFinder =
                    protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);

                list.push(whileFn(elementFinder, index));
            });
            return protractor.promise.all(list).then(function(resolvedList) {
                var filteredElementList = [];
                for (var index = 0; index < resolvedList.length; index++) {
                    if (!resolvedList[index]) {
                        break;
                    }
                    filteredElementList.push(parentWebElements[index])
                }
                return filteredElementList;
            });
        });
    };
    return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};

The usage is very similar to filter():

element.all(by.css("ul li a")).takewhile(function (elm) {
    return elm.getText().then(function (text) {
        return text;
    });
});

FYI, proposed to make takewhile() built-in.

Community
  • 1
  • 1
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195