Actions now use run.join not run


#1

Mostly documenting this for future travelers…here goes.

So in this commit:

The internal ember-glimmer package changed from using Ember.run to using Ember.run.join. If you aren’t familiar with the difference the former creates a nested “runloop” and the latter will either join an existing “runloop” or if one is not available will create one.

99% of the time when people use Ember.run they could be better served by using Ember.run.join. This is because when you call Ember.run you ALWAYS nest a whole runloop which carries with it a lot of unnecessary book keeping and work.

When I updated my project from 3.1 to 3.2 this change broke 9 tests. Just to be clear; each of these tests were incorrectly written. But the general gist was something like this (pay specific note the when you click .find-by-something it triggers a closure action):

this.render(hbs`
  {{something}}
`);

await $('.find-by-something').click();

assert.ok(/* something happens here as a result of a call to an action */);

The solution is to rewrite like this:

this.render(hbs`
  {{something}}
`);

$('.find-by-something').click();

await settled().then(function() {
  assert.ok(/* something happens here as a result of a call to an action */);
});

First of all, the first example likely never should have worked. But let’s talk about what is happening.

When you await a jQuery click you aren’t really doing anything that Ember knows or cares about because $(...).click() doesn’t return a promise. So don’t think too hard about that line.

It does however trigger a runloop, since Ember wraps events in a runloop to handle side effects, and to be generally helpful. Previously, before the commit linked above, this would create a runloop and then the action would trigger which would create a nested runloop.

The nested runloop would completely flush before the outer or parent runloop (instantiated by Ember’s click handler). Which means you’d end up with a guarantee that Ember’s action and all of it’s associated side effects would be handled before the click runloop (the outermost runloop) finished. Which sorta simulates synchronous behavior.

This is BAD. Because it means that we are blocking rendering while those actions fire.

So the commit fixes this by scheduling the action into the current runloop with join.

A simplified example of the difference:

This really would have been a lot easier with the new test helpers. Since helpers will handle the settling of runloops out of the box. See more details here about that: https://guides.emberjs.com/v3.1.0/testing/testing-components/

Thanks to @rwjblue and @kpfefferle for helping me talk through what was happening in these cases.


#2

Awesome walk through, thanks for writing it up!


#3

Could this be rewritten like the following to take full advantage of the async/await functionality as well as the Ember Test Helpers?

this.render(hbs`
  {{something}}
`);

await click('.find-by-something');

// If the action is doing things outside of the Ember Runloop, 
// sometimes you need the following line uncommented 
// (or preferably fix the underlying code if possible)
// await settled();

// Shouldn't need to put the assert in a .then function since 
// the await will just wait for completion before executing
assert.ok(/* something happens here as a result of a call to an action */);

#4

Yeah, this likely would be fixed with the ember-test-helpers as per the guides. The ember-test-helpers click test helper automatically wraps the click events in a call to settled as far as I know. So that’d work perfectly, I think.


#5

Ya, the examples in the original post were in the “legacy” moduleForComponent style. The specific failures that @rondale_sc mentioned would not have happened in the newer style of tests because await click(...) would have not relied on clicking the action doing a sync rerender.