3

So... I have some methods. Each method returns a promise.

     myAsyncMethods: {
          myNavigate () {
            // Imagine this is returning a webdriverio promise
            return new Promise(function(resolve){ 
              setTimeout(resolve, 1000);
            })
          },
          myClick () {
            // Imagine this is returning a webdriverio promise
            return new Promise(function(resolve){
              setTimeout(resolve, 2000);
            })
          }
        }

I'm trying to make end to end tests, so the prom chain must be linear (first click, next navigate, etc)

For now, I can do this...

makeItFluent(myAsyncMethods)
    .myNavigate()
    .myClick()
    .then(() => myAsyncMethods.otherMethod())
    .then(() => /*do other stuff*/ )

...with ES6 proxy feature:

function makeItFluent (actions) {
    let prom = Promise.resolve();
    const builder = new Proxy(actions, {
        get (target, propKey) {
            const origMethod = target[propKey];

            return function continueBuilding (...args) {
                // keep chaining promises
                prom = prom.then(() => (typeof origMethod === 'function') && origMethod(...args));

                // return an augmented promise with proxied object
                return Object.assign(prom, builder);
            };
        }
    });

    return builder;
};

But, the thing I cannot do is the following:

makeItFluent(myAsyncMethods)
    .myNavigate()
    .myClick()
    .then(() => myAsyncMethods.otherMethod())
    .then(() => /*do other stuff*/ )
    .myNavigate()

Because then is not a proxied method, and thus it does not return myAsyncMethods. I tried to proxy then but with no results.

Any idea?

thanks devs ;)

  • You really *should not* proxy `then` to decorate to return a non-promise anyway. If everyone did this, references to your methods would leak around with every promise result. – Bergi May 22 '17 at 16:06
  • 1
    Your `makeItFluent` thing does not allow branching, it always builds a linear `prom` chain. Avoid the imperative assignment. – Bergi May 22 '17 at 16:13
  • `Object.assign` does destroy your proxy. – Bergi May 22 '17 at 16:13
  • Thanks @Bergi, I know this is not the best approach. But I have to figure this out. – Serguey Arellano May 22 '17 at 18:12
  • I can't see the need of branching in `makeItFluent`, can you explain? I just need to return a proxy of `myAsyncMethods`, right? :s Whenever a method is called, it returns a promise with proxied methods, because I can see the traces inside the getter when I execute this stuff. You got me confused there because it works... maybe I can't see your point – Serguey Arellano May 22 '17 at 18:21
  • `var x = makeItFluent(myAsyncMethods); x.method1(); x.method2()` does not work, it should execute them in parallel instead of after each other. – Bergi May 22 '17 at 23:23
  • @SergueyArellano can you share some *actual* code? What is `myAsyncMethods` supposed to be? Does each function return a promise? This seems like an [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – you're asking about code you wrote or trying to write instead of describing the problem you're trying to solve. – Mulan May 23 '17 at 00:59
  • @SergueyArellano what sort of data do you expect to move through this fluent api? from the looks of it now, all functions are expected to have side effects otherwise nothing would even happen – Mulan May 23 '17 at 01:00
  • @SergueyArellano `method1` and `method2` are expected to return a chainable object, but how do you eventually get data out? If an async method ever enters the chain (it sounds like it will), the returned object must always be `.then`able. – Mulan May 23 '17 at 01:01
  • @SergueyArellano update your question with some code that runs – even if it's silly functions like `async x => x + 1` and `async x => x.toUpperCase()` etc. It's OK that the code doesn't work (it's why you're asking a question), but we need to know *how* your code is failing to meet your expectations. Show us a *complete* example of what you want the API to look like and we can work from there. – Mulan May 23 '17 at 01:04
  • Thanks @Bergi @naomik, I updated the question so you can reproduce it (I think so). I'm in an end to end test environment. I'm trying to implement a chainable pattern with page objects under webdriverio, selenium, etc. Each method of webdriverio returns a promise. The execution of each method must be linear because a `click` should be executed after a `navigate` in order the test to succeed. – Serguey Arellano May 23 '17 at 09:40
  • It would be much better if you add runable example in snippet into the qusetion. – Qwertiy Oct 11 '17 at 13:28
  • Why can't you just put `myNavigate()` inside a `then()`? Seems like you're overcomplicating things here. – JordyvD Nov 05 '17 at 13:54

2 Answers2

2

I would return wrapped Promises from yourAsyncMethods which allows mixing of sync and async methods with Proxy and Reflect and executing them in the correct order :

/* WRAP PROMISE */
let handlers;
const wrap = function (target) {
    if (typeof target === 'object' && target && typeof target.then === 'function') {
        // The target needs to be stored internally as a function, so that it can use
        // the `apply` and `construct` handlers.
        var targetFunc = function () { return target; };
        targetFunc._promise_chain_cache = Object.create(null);
        return new Proxy(targetFunc, handlers);
    }
    return target;
};
// original was written in TS > 2.5, you might need a polyfill :
if (typeof Reflect === 'undefined') {
    require('harmony-reflect');
}
handlers = {
    get: function (target, property) {
        if (property === 'inspect') {
            return function () { return '[chainable Promise]'; };
        }
        if (property === '_raw') {
            return target();
        }
        if (typeof property === 'symbol') {
            return target()[property];
        }
        // If the Promise itself has the property ('then', 'catch', etc.), return the
        // property itself, bound to the target.
        // However, wrap the result of calling this function.
        // This allows wrappedPromise.then(something) to also be wrapped.
        if (property in target()) {
            const isFn = typeof target()[property] === 'function';
            if (property !== 'constructor' && !property.startsWith('_') && isFn) {
                return function () {
                    return wrap(target()[property].apply(target(), arguments));
                };
            }
            return target()[property];
        }
        // If the property has a value in the cache, use that value.
        if (Object.prototype.hasOwnProperty.call(target._promise_chain_cache, property)) {
            return target._promise_chain_cache[property];
        }
        // If the Promise library allows synchronous inspection (bluebird, etc.),
        // ensure that properties of resolved
        // Promises are also resolved immediately.
        const isValueFn = typeof target().value === 'function';
        if (target().isFulfilled && target().isFulfilled() && isValueFn) {
            return wrap(target().constructor.resolve(target().value()[property]));
        }
        // Otherwise, return a promise for that property.
        // Store it in the cache so that subsequent references to that property
        // will return the same promise.
        target._promise_chain_cache[property] = wrap(target().then(function (result) {
            if (result && (typeof result === 'object' || typeof result === 'function')) {
                return wrap(result[property]);
            }
            const _p = `"${property}" of "${result}".`;
            throw new TypeError(`Promise chain rejection: Cannot read property ${_p}`);
        }));
        return target._promise_chain_cache[property];
    },
    apply: function (target, thisArg, args) {
        // If the wrapped Promise is called, return a Promise that calls the result
        return wrap(target().constructor.all([target(), thisArg]).then(function (results) {
            if (typeof results[0] === 'function') {
                return wrap(Reflect.apply(results[0], results[1], args));
            }
            throw new TypeError(`Promise chain rejection: Attempted to call ${results[0]}` +
                ' which is not a function.');
        }));
    },
    construct: function (target, args) {
        return wrap(target().then(function (result) {
            return wrap(Reflect.construct(result, args));
        }));
    }
};
// Make sure all other references to the proxied object refer to the promise itself,
// not the function wrapping it
Object.getOwnPropertyNames(Reflect).forEach(function (handler) {
    handlers[handler] = handlers[handler] || function (target, arg1, arg2, arg3) {
        return Reflect[handler](target(), arg1, arg2, arg3);
    };
});

You would use it with your methods like

myAsyncMethods: {
      myNavigate () {
        // Imagine this is returning a webdriverio promise
        var myPromise = new Promise(function(resolve){ 
          setTimeout(resolve, 1000);
        });
        return wrap(myPromise)
      },
// ...

Please note two things :

You can now mix it like

FOO.myNavigate().mySyncPropertyOrGetter.myClick().mySyncMethod().myNavigate() ...
sebilasse
  • 4,278
  • 2
  • 37
  • 36
2

https://michaelzanggl.com/articles/end-of-chain/

A promise is nothing more than a "thenable" (an object with a then() method), which conforms to the specs. And await is simply a wrapper around promises to provide cleaner, concise syntax.

class NiceClass {
  promises = [];

  doOne = () => {
    this.promises.push(new Promise((resolve, reject) => {
        this.one = 1;
        resolve();
    }));
    return this;
  }

  doTwo = () => {
    this.promises.push(new Promise((resolve, reject) => {
      this.two = 2;
  
      resolve();
    }));

    return this;
  }

  async then(resolve, reject) {
    let results = await Promise.all(this.promises);
    resolve(results);
  }

  build = () => {
    return Promise.all(this.promises)
  }
}

Them you can call it in both ways.

(async () => {
  try {
    let nice = new NiceClass();
    let result = await nice
      .doOne()
      .doTwo();

    console.log(nice);

    let nice2 = new NiceClass();
    let result2 = await nice2
      .doOne()
      .doTwo()
      .build();

    console.log(nice2, result2);
  } catch(error) {
    console.log('Promise error', error);
  }
})();
BlaShadow
  • 11,075
  • 7
  • 39
  • 60