Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Making ElementArrayFinder from an array of ElementFinders #2538

Closed
alecxe opened this issue Sep 25, 2015 · 11 comments
Closed

Making ElementArrayFinder from an array of ElementFinders #2538

alecxe opened this issue Sep 25, 2015 · 11 comments

Comments

@alecxe
Copy link
Contributor

alecxe commented Sep 25, 2015

This is basically a follow-up to the resolved SO topic: http://stackoverflow.com/questions/32599679/making-elementarrayfinder-from-an-array-of-elementfinders.

Would it make sense to have fromArray() method available on ElementArrayFinder built-in?

Thanks.

Feel free to close this one if you think this is not something we can benefit from.

@sjelin
Copy link
Contributor

sjelin commented Sep 25, 2015

Not relevant but I'm curious: have you actually tried the SO answer? I think it might be slightly wrong but I'm not sure without running it.

@stalniy
Copy link
Contributor

stalniy commented Sep 25, 2015

I have similar implementation for my project. Basically it's convenient to have 2 options fromArray and fromPromise in case you can't retrieve elements synchronously.

@alecxe
Copy link
Contributor Author

alecxe commented Sep 25, 2015

@sjelin yes, it worked for me. What do you think can go wrong? Thanks.

@stalniy could you please share what do you have for fromArray() and fromPromise()? Thank you.

@sjelin
Copy link
Contributor

sjelin commented Sep 25, 2015

I just wasn't sure about how they were handling the promises.

@stalniy
Copy link
Contributor

stalniy commented Oct 1, 2015

@alecxe sure:

element.all.of = function(finders) {
  var combinedLocator = {
    toString: function() {
      return _.invoke(this.locators, 'toString').join(', ');
    }
  };
  var newElements = q.when(finders);
  var arrayFinder = new ElementArrayFinder(null, function() { return newElements; }, combinedLocator);

  // TODO: consider pull-request into protractor.js
  arrayFinder.asElementFinders_ = arrayFinder.getWebElements; // cause we have ElementFinders in newElements
  newElements.then(function(finders) {
    combinedLocator.locators = _.invoke(finders, 'locator');
  });

  return arrayFinder;
};

// usage Array
element.all.of([ element(by.id), element(by.name('button1'), element(by.name('button2') ]);

// usage promise
var buttons = element.all(by.repeater('item in items')).reduce(function(allButtons, item) { //
   return allButtons.concat(findButtonsIn(item));
 }, []);

element.all.of(buttons);

@hankduan
Copy link
Contributor

I'm still not convinced this function is needed. Looking at your stackoverflow issue, here's your original code:

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();
        }
    });
}());
// Now you want to convert tableItems to an ElementArrayFinder like:
// var myEls = ElementArrayFinder.fromArray(tableItems);

You can just rewrite it with filter:

var els = element.all(by.css('ul li a'));
var filteredEls = els.filter(function(el) {
  return el.getText().then(function(text) {
    return !!text;
  })
});
// filteredEls is exactly what you want

The rewrite makes the code shorter and easier to understand. Can you provide me with an example where the fromArray method makes life easier?

@stalniy
Copy link
Contributor

stalniy commented Oct 28, 2015

The good example for this is when you want to aggregate elements that were found by different locators but logically relate to one thing (or when you have a promise which returns an array of finders, as in case with ElementFinder#reduce)

For example,

@hankduan
Copy link
Contributor

Ok fair enough. It looks like you already have the code ready, can you send it as a PR and I'll take a look?

@juliemr
Copy link
Member

juliemr commented Jul 14, 2016

Closing this as stale, with no example or PR.

@juliemr juliemr closed this as completed Jul 14, 2016
@keithrz
Copy link

keithrz commented Apr 28, 2017

@juliemr @hankduan I'm wondering if I have to do the same thing as the SO solution, too.

Use case: I have to get text from a tooltip.

Requirements:

  1. Not all the text, just the left-hand labels, and verify the text is correct. The left-hand labels are retrieved via an element.all().
  2. Sometimes I want to get the elements from this element.all() call, sometimes I want to get the text
  3. In order to see the text from a tooltip, I have to click on certain elements and/or move the mouse to the proper location before the element.all call.

Because of Req 2, I want a function that will always return an ElementArrayFinder. That way, I can have another function that calls getText() on the result of my function, a different function that calls click() on the result, etc.

Because of Req 3, and because I'm doing this in a page object method, I have to chain together a click with a call to element.all(). I'm doing this in a page object method in order to reuse code, but perhaps I'm making my page objects too complex? If the click() call was made in the spec itself, I'd get the protractor synchronous magic to help me out.

Here's a simple test case, which tries to get elements in the "color" option selector in the protractor test page. To relate this test case to my use case, imagine that the color options are the left-hand labels in the tooltip, and getSomethingToClick() was clicking on whatever makes the tooltip pop up.
I was having issues with this page due to #4233, but this test doesn't actually need to find anything on the page to show the discrepancy here.

// specs/test.spec.js

// protractor config file uses baseUrl: 'http://www.protractortest.org/testapp/ng1/'
// baseUrl does not actually matter, only checking elementarrayfinder

const protractorTestPage = require('../pages/test-page.js');

describe('Element Array Finder', () => {
  beforeAll(() => {
    browser.get('/testapp/ng1/#/form');
  });

  it('produces an ElementArrayFinder normally', () => {
    const returnVal = protractorTestPage.getColorOptions();
    const returnType = returnVal.constructor.name;
    //succeeds
    expect(returnType).toEqual('ElementArrayFinder');
  });

  it('produces an array of element finders when passed through a promise chain', () => {
    const returnVal = protractorTestPage.getColorOptionsAtEndOfChain();
    const returnType = returnVal.constructor.name;
    // succeeds
    expect(returnType).toEqual('ManagedPromise');

    //fails
    returnVal.then((returnValPromised) => {
      const promisedType = returnValPromised.constructor.name;
      expect(promisedType).toEqual('ElementArrayFinder');
    });
  });

});




// pages/test-page.js

module.exports = {

  getSomethingToClick() {
    return this.getColorOptions()
      // .last()
      ;
  },

  getColorOptionsAtEndOfChain() {
    return this.getSomethingToClick()
      .click()
      .then(() => {
        return this.getColorOptions();
      });
  },

  getColorOptions() {
    return element.all(by.model('color'));
  }
};

@keithrz
Copy link

keithrz commented Apr 29, 2017

I ended up writing a Protractor util:

//// protractor-utils.js

module.exports = {
  buildElementArrayFinderFromPromise(promiseOfElementFinders) {
    const getPromise = function() {
      return promiseOfElementFinders;
    };
    return new protractor.ElementArrayFinder(protractor, getPromise);
  }
};

protractor.promise.Promise.prototype.asElementArrayFinder = function() {
  return module.exports.buildElementArrayFinderFromPromise(this);
};

And modified my getColorOptionsAtEndOfChain() method:

getColorOptionsAtEndOfChain() {
    return this.getSomethingToClick()
      .click()
      .then(() => {
        return this.getColorOptions();
      }).asElementArrayFinder();
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants