Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generators, promises, Nightmare #537

Closed
kuraga opened this issue Mar 15, 2016 · 11 comments

Comments

Projects
None yet
3 participants
@kuraga
Copy link

commented Mar 15, 2016

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

This comment has been minimized.

Copy link
Collaborator

commented Mar 16, 2016

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

This comment has been minimized.

Copy link
Author

commented Mar 16, 2016

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

This comment has been minimized.

Copy link
Collaborator

commented Mar 16, 2016

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:

  • co uses next internally
  • co promisifies all .next() values
  • co uses an internal next function to determine if the generator is complete and to resolve values if complete, and if not, issue a call to .then() on the value that has been made into a promise

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

This comment has been minimized.

Copy link
Author

commented Mar 17, 2016

@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

This comment has been minimized.

Copy link
Collaborator

commented Mar 17, 2016

@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

This comment has been minimized.

Copy link
Author

commented Mar 18, 2016

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

This comment has been minimized.

Copy link
Collaborator

commented Mar 18, 2016

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

This comment has been minimized.

Copy link
Author

commented Mar 18, 2016

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

This comment has been minimized.

Copy link
Collaborator

commented Mar 18, 2016

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

This comment has been minimized.

Copy link
Collaborator

commented Mar 30, 2016

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

@cipri-tom

This comment has been minimized.

Copy link

commented Jan 14, 2018

@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 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.