segment-boneyard / nightmare

A high-level browser automation library.
https://open.segment.com
19.54k stars 1.08k forks source link

Generators, promises, Nightmare #537

Closed kuraga closed 8 years ago

kuraga commented 8 years ago

Good day!

  1. Why does
var exists = yield page
  .exists('h1.title');
assert(exists);

work but only with yield? What's the magic here?

  1. Can I get a Promise instance without calling Nightmare.prototype.then?

Thanks!

rosshinkley commented 8 years ago

I presume this is wrapped in a generator and probably run with vo or co or something similar?

Nightmare (partially) implements promises, as you have already discovered. A very condensed, hand-waving description: the .then() call is called by yield, the result of which is returned to exists. Without yield, exists will be the Nightmare instance itself.

Your example, rewritten with the promise implementation for what it's worth:

page.exists('h1.title')
  .then(function(exists){
     assert(exists);
  });

As for your other question about getting a Promise instance without calling .then(), I'm curious why you would want to do that. You could probably use the Nightmare instance itself, but that might have side effects you don't want. What are you trying to accomplish?

kuraga commented 8 years ago

the .then() call is called by yield

That's logical but I don't see this this behavior in articles about generators... About .next() call only not .then(). Who does call .then()?

I want to understand: 1) the schema (of Nightmare and generators), 2) if I can use Nightmare with vanilla NodeJS and without Mocha.

Thanks!

rosshinkley commented 8 years ago

That's logical but I don't see this this behavior in articles about generators...

My original hand-waving explanation buries almost all of the complexity involved, and frankly, I'm not sure I have a great grasp on it myself. I'll give it a try, at least.

About .next() call only not .then(). Who does call .then()?

This requires a longer explanation.


Iterators

I think it's important to first talk about iterators, the return type of generators. They are comprised of two parts: a done member, which is true if the iterator is past the end of the iterated sequence, and a value member, which can be any value. With that in mind, let's take a look at an example:

var run = function * () {
  yield 'hello';
};

var runner = run();
var result = runner.next();
console.log(result);
result = runner.next();
console.log(result);

...which will output:

{ value: 'hello', done: false }
{ value: undefined, done: true }

Here, we can see that yield returns "hello" and waits for .next() to be called. When it is, the generator iterates with done being true.

Promises and iterators

You can also yield on promises, although it's not as pretty. Calling .then() is up to the calling code. The following example is a bit trickier:

var run = function * () {
  var promise = new Promise(function(resolve) {
    return resolve('hello')
  });
  var x = yield promise;
  x += '!';
  return x;
};

var runner = run();
var result = runner.next();
console.dir(result)
result.value.then(function(resolvedValue) {
  console.log(resolvedValue);
  result = runner.next(resolvedValue + ' world');
  console.dir(result);
});

...which will output:

{ value: Promise { 'hello' }, done: false }
hello
{ value: 'hello world!', done: true }

It's a little bit uglier, but not all that different than before: the generator function yields the promise, which the calling code resolves. The calling code then calls .next() with the resolved value with some additional information, passing that argument back to x. Finally, x has "!" added and returned for the last return value.

co and .then()

Now, with that background out of the way, I'm going to focus on co because I think vo relies on it (via wrapped) for generators. Additionally, I think co is the most straightforward way to answer your question.

co wraps promises up for you so you can execute generator code without intermediate calls to .next(). It will return the resolved value back to .next() and ultimately back to what yield is setting or being passed to (x in our previous example). You lose the ability to run intermediate code, but gain the ability to run complex generators without having to manage the yield chain yourself. In your original question, var result = yield page.exists('h1.title') works because co handles .then() for you: since Nightmare is a thenable, co will resolve the value returned by .exists().

Further Reading

If you're interested, I've put together a couple of places in the co source that might help your understanding:

Hopefully the above makes sense. Please let me know if you have questions.


if I can use Nightmare with vanilla NodeJS and without Mocha.

Of course! As of Node 4.x, promises are natively supported. This means you can use vanilla javascript to control Nightmare. For completeness, the decision to move to using native promises over something like co is explained (at length) in #491.

An example on using Nightmare with native promises is provided in the readme.

kuraga commented 8 years ago

@rosshinkley big thanks about explanation!

if I can use Nightmare with vanilla NodeJS and without Mocha.

Of course! As of Node 4.x, promises are natively supported.

I meant "without mocha-generators, mocha, co and vo".

Please note that the examples are using the mocha-generators package for Mocha, which enables the support for generators.

Is this description wrong here? Do you mean "which wraps generator function with co/vo" instead of "enables the support for generators"? NodeJS 4.2 does support generators (so they are already "enabled") but seems like code doesn't work without mocha-generators.

rosshinkley commented 8 years ago

@kuraga No problem.

I meant "without mocha-generators, mocha, co and vo".

You don't need co or vo to run Nightmare. ES6 promises are native - meaning no library is required - and work fine. You can run Nightmare with callbacks, but it's not directly supported. Maybe I don't understand what the problem is?

Is this description wrong here? Do you mean "which wraps generator function with co/vo" instead of "enables the support for generators"? NodeJS 4.2 does support generators (so they are already "enabled") but seems like code doesn't work without mocha-generators.

I think it's a little misleading. Generators are native to Node 4.x. I think the point the documentation is trying to make is that the tests are written using generators and run using mocha-generators which I believe works like co or vo.

kuraga commented 8 years ago

The problem is that yield promise doesn't call promise.then() in vanila NodeJS (if I use example with generators but without mocha-generators. Promises and generators are enabled). Seems like it just yields promise instead.

rosshinkley commented 8 years ago

The problem is that yield promise doesn't call promise.then() in vanila NodeJS...

No, it does not. If you use vo or co or mocha-generators, it takes care of that for you. If you wanted to use yield and .then() in vanilla JS with no dependencies, you'd have to manage the promise chain yourself. That's the point I was trying to make in this comment.

I also feel like maybe there's a misunderstanding of how Nightmare is intended to be used. You don't need to use yield. Using generators adds convenience, but isn't strictly necessary. You could use the resolved callback of .then() to get values from the Nightmare thenable. That's how the example at the top of the readme works.

Backing all the way up to your original example, you could check for/assert for existence without yield or the need for generators. I know I wrote it before, but presented again, consider:

page
  .exists('h1.title')
  .then(function(exists){
    assert(exists);
  })

This will take the existing page, queue up an existence check, then run the queue and call back with the result of that existence check. Is this method causing problems? Do you have a more complete example?

kuraga commented 8 years ago

No, it does not. If you use vo or co or mocha-generators, it takes care of that for you. If you wanted to use yield and .then() in vanilla JS with no dependencies, you'd have to manage the promise chain yourself.

That's exactly I wanted to hear. Remember my words:

Please note that the examples are using the mocha-generators package for Mocha, which enables the support for generators.

Is this description wrong here? Do you mean "which wraps generator function with co/vo" instead of "enables the support for generators"?

Let's precise documentation?

Thanks, @rosshinkley !

rosshinkley commented 8 years ago

That's exactly I wanted to hear. Remember my words:

I understand what your point is now, I think. The documentation doesn't explicitly say how and when to use yield, generators, etc. I don't think the documentation should: Nightmare is designed for use out of the box with plain ES6 promises. It's up to you, the user, to determine what's best for your application's flow control.

I'd be curious to hear if anyone is using ES6 managing promises with yields themselves. I doubt yielding on a promise outside of a flow control library is common, but don't have any evidence to back that assumption.

Let's precise documentation?

We talked about documentation at length in #491 (it's a very, very long read). Ultimately, this is why I started nightmare-examples - it was evident the documentation and usage tripped people up, so adding supplementary documentation and examples here and there to try and clear up some of the common problems made sense. How co et al interacts with yield and promises might make for a useful addition.

rosshinkley commented 8 years ago

I believe this issue is resolved. If this is still a problem, feel free to reopen/open another issue.

cipri-tom commented 6 years ago

@rosshinkley thank you for the detailed explanation about how co works. The references to its internals are great !

Please consider linking that comment in the co example, as well as Loops and everywhere else vo/co appears. Hell, even on the co official documentation !

Thank you !