10

q library has this neat feature to resolve and spread multiple promises into separate arguments:

If you have a promise for an array, you can use spread as a replacement for then. The spread function “spreads” the values over the arguments of the fulfillment handler.

return getUsername()
    .then(function (username) {
        return [username, getUser(username)];
    })
    .spread(function (username, user) {

    });

In protractor, we are trying to use the built-in protractor.promise coming from WebDriverJS.

The Question:

Is it possible to have the "spread" functionality with protractor.promise?

Example use case:

We've implemented a custom jasmine matcher to check if an element is focused. Here we need to resolve two promises before making an equality comparison. Currently, we are using protractor.promise.all() and then():

protractor.promise.all([
    elm.getId(),
    browser.driver.switchTo().activeElement().getId()
]).then(function (values) {
    jasmine.matchersUtil.equals(values[0], values[1]);
});

which ideally we'd like to have in a more readable state:

protractor.promise.all([
    elm.getId(),
    browser.driver.switchTo().activeElement().getId()
]).spread(function (currentElementID, activeElementID) {
    return jasmine.matchersUtil.equals(currentElementID, activeElementID);
})
Community
  • 1
  • 1
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • How do you expect this to happen if these are two different libraries? Unless WebDriverJS promise is wrapped in Q or Bluebird you can't _get functionality_ of another library – Kirill Slatin Aug 07 '15 at 23:39
  • @KirillSlatin that's what I am actually asking about. How can we extend the `protractor.promise` and is it doable? Also, I haven't tried it yet, but what if we replace `protractor.promise` with `q` on the fly, how would it affect the protractor test run etc. – alecxe Aug 07 '15 at 23:58
  • 1
    hmm, that might make sense to replace the promise engine from the very beginning. I thought of wrapping a specific promise instance. Something similar to bluebird's `promisify` – Kirill Slatin Aug 08 '15 at 00:17
  • The first reckless idea that comes in mind is to simply assign `protractor.promise` with `q` before tests start. According to docs protractor is a wrapper around WebDriverJS, it would be impossible to separate them. All we can hope is that WebDriverJS promises are A+ compliant and there is no vendor-specific usage. Thus Bluebird or Q should fit. However I don't have a ready protractor setup at the moment to verify this – Kirill Slatin Aug 08 '15 at 00:31
  • 1
    However I think this might not work, Protractor makes use of ControlFlow, which is implemented inside WebDriverJS. And `Protractor.promise` most likely is just a reference, so changing it won't help. What really should be changed to slot in a new promise engine is `require('webdriver').promise` somehow – Kirill Slatin Aug 08 '15 at 00:59
  • @KirillSlatin thanks for the insight so much, I'll play around with that in our protractor setup. – alecxe Aug 08 '15 at 01:06
  • 1
    Thought: Starting with a Protractor promise, if you need some sugar method offered by Q, then you could try coercing to Q, using Q's sugar, then coercing back into Protractor, eg. `var newProtratorPromise = protractor.promise.when(Q.when(someProtractorPromise).sugarMethod(...));`. Probably not worth the bother for your `.spread()` case but worth considering for more involved sugar, providing Protractor's "ControlFlow" aspects are not lost in the double-shuffle. – Roamer-1888 Aug 08 '15 at 10:53
  • 1
    @KirillSlatin actually, making webdriverjs and protractor to use `q` worked for me. I've posted it as an answer and created a [github thread](https://github.com/angular/protractor/issues/2412#issuecomment-129035147). I'm really not sure about the problems it can cause, but so far, my tests work as before. Thanks again for the participation. – alecxe Aug 08 '15 at 23:14
  • What's the bounty good for? What new answers would you like to get, how are the current ones not satisfying? Of course, other approaches would be ES6 destructuring or just amending the protactor promise lib with a `.spread` method. – Bergi Sep 12 '15 at 14:31
  • @Bergi basically, here I just want to draw attention to it and share an interesting problem with others. As a bonus, the bounty would be awarded to Michael or someone with an interesting alternative solution we haven't thought of. Thanks! – alecxe Sep 12 '15 at 15:32

2 Answers2

7

It may come a bit ugly to use, but you can define an independent helper function, which can be passed to then() as a parameter and have a callback, which is usually passed to then() to be passed to it. This function will then convert array value to function arguments:

protractor.promise.all([
    elm.getId(),
    browser.driver.switchTo().activeElement().getId()
]).then(spread(function (currentElementID, activeElementID) {
    // ---^^^----- use helper function to spread args
    jasmine.matchersUtil.equals(currentElementID, activeElementID);
}));


// helper function gets a callback
function spread(callback) {
    // and returns a new function which will be used by `then()`
    return function (array) {
        // with a result of calling callback via apply to spread array values
        return callback.apply(null, array);
    };
}

You can still chain it with another then() and provide rejection callbacks; it keeps all the behavior of Protractor promises the same, but just converts array of values to arguments.

Drawbacks are that it is does not have a perfect look like in your example (not .all().spread() but .all().then(spread()) ) and you'll probably have to create a module for this helper or define it globally to be able to use it easily in multiple test files.

Update:

With ES2015 it is possible to use destructuring assignment along with then():

protractor.promise.all([
    elm.getId(),
    browser.driver.switchTo().activeElement().getId()
]).then(function (values) {
    // Destructure values to separate variables
    const [currentElementID, activeElementID] = values; 
    jasmine.matchersUtil.equals(currentElementID, activeElementID);
}));
Michael Radionov
  • 12,859
  • 1
  • 55
  • 72
  • Yeah, thanks! This is something I was thinking about to workaround it - make a wrapper to support spreading/unpacking/destructing. I've also posted how I've solved it, but there is an open question if it's actually safe to replace `protractor.promise` with `q`..what can go wrong?.. – alecxe Aug 08 '15 at 23:17
  • 2
    I took a brief look at the source codes and like a developer said in your Github issue, it has a thing called control flow used inside promise methods ([desc](https://github.com/SeleniumHQ/selenium/blob/master/javascript/webdriver/promise.js#L1193)). I guess it is responsible for the order of the specs and/or an ability to run tests in parallel in multiple browser instances, but I'm not sure. These flows are initialized on a high level, [when binding drivers](https://github.com/SeleniumHQ/selenium/blob/01399fffecd5a20af6f31aed6cb0043c9c5cde65/javascript/node/selenium-webdriver/builder.js#L454) – Michael Radionov Aug 08 '15 at 23:40
4

TL;DR Apparently, it's not entirely safe to replace protractor.promise with q. For instance, I've got a hanging test run once I've decided to extend ElementArrayFinder:


Old answer:

Here is what I've done to solve it.

I've replaced protractor.promise with q on the fly (not sure if it's actually safe to do):

onPrepare: {
    protractor.promise = require("q");
},

But, nothing broke so far and now I'm able to use spread() and other syntactic sugar provided by q through protractor.promise:

toBeActive: function() {
    return {
        compare: function(elm) {
            return {
                pass: protractor.promise.all([
                    elm.getId(),
                    browser.driver.switchTo().activeElement().getId()
                ]).spread(function (currentElementID, activeElementID) {
                    return jasmine.matchersUtil.equals(currentElementID, activeElementID);
                })
            };
        }
    };
}

Relevant github thread: protractor.promise to use q.

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