Ember test with async await functions not working as intended

Can someone explain to me, why the following code won’t wait for asynchronous click function?

import { module, test } from 'qunit';
import { click, visit, currentURL, fillIn, pauseTest } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';

module('Acceptance | login', async function(hooks) {
  setupApplicationTest(hooks);

  test('should login', async function(assert) {
    await visit('/login');
    await fillIn('input#email', 'martin.barilik@noh2o.com');
    await fillIn('input#password', '123456');
    await click('button[type="submit"]');
    // await pauseTest();
    assert.equal(this.element.querySelector('div.list-group a').textContent, 'Dashboard');
  });
});

The button is calling async authenticate function:

@action
async authenticate(e) {
e.preventDefault();

let { email, password } = this;
try {
  await this.session.authenticate('authenticator:custom', email, password);
 } catch(error) {
  
   this.errorMessage = error.error || error;
 }
}

If i uncomment await pauseTest(), i can see the page being loaded and after resumeTest() i get successful assertion, but without it it fails with error:

Source: TypeError: Cannot read property 'textContent' of null

Which indicates, that the await is not waiting and assert.equal is executed before the page is loaded.

edit: i am using remote adapter as a backend (sailsjs server)

What am i doing wrong?

await click() is only waiting for the click event to get handled. It doesn’t wait for all asynchronous work scheduled by the click event to finish.

I think you can probably await settled() after await click() and that would cover this.

Hi @ef4,

await settled(); doesnt wait for click to be resolved as well. …

I had to use this code:

await click('button[type="submit"]');
await waitFor('div.list-group a', 4000);
await settled();
assert.equal(this.element.querySelector('div.list-group a').textContent, 'Dashboard');

Strange workaround, i don’t think it’s right …

@mArtinko5MB out of curiosity what version of ember-test-helpers are you on? IIRC there were some issues fixed with some of the async helpers at some point…

In the past the answer was usually to make sure async stuff was properly scheduled on the runloop but I thought we were past most of that now. What kind of stuff is happening in your authenticate method?

I was 1.7 version, did update them to the latest version. But it didn’t help.

Can not call .lookup after the owner has been destroyed

Keep getting this message randomly.

I am using ember-simple-auth with custom authenticator, which is pretty basic. Just sends login request to backend API, catching error when response code is other than 200.

I am kinda frustrated by the tests in app … Feels like it’s useless here. Everything is based on promises of requests to backend, and if it’s that hard to complete even one test, i will skip the whole tests acceptance and units.

:wave: @mArtinko5MB, sorry for the confusion here!

tldr; add @ember/test-waiters to your application, and update your code snippet to:

import { waitFor } from '@ember/test-waiters';

// ...snip...

@action
@waitFor
async authenticate(e) {
  e.preventDefault();

  let { email, password } = this;
  try {
    await this.session.authenticate('authenticator:custom', email, password);
  } catch(error) {
  
    this.errorMessage = error.error || error;
  }
}

OK, so now that that is fixed, lets dig into the main issue you were running into: nothing in the system knows that it should “wait” for the async action to complete!

Lets break down what is waited for automatically by calls to await settled() (which is internally used by await click(...) as well). Settledness in this context is defined as:

  • Has no active runloop
  • Has no pending runloop timers (e.g. usages of schedule from @ember/runloop)
  • Has no pending test waiters (this one is important, we will come back to it)
  • Has no pending jQuery requests (generally not applicable in modern octane apps, since they do not use jQuery)
  • Has no pending router transitions (e.g. all model hooks are resolved, any redirects have been absorbed, etc)

One very important thing to call out here: we do not wait for “all promises to be resolved”! (Doing this is basically impossible anyways…)

OK, now back to your code snippet, in your test you are click(someSelector) that invokes an async action. Since the thing that action does does not fall into any of the “settledness delaying” categories, the test happily continues before that authentication is completed.

Now, lets get back to “has pending test waiters”. This is where @ember/test-waiters. @ember/test-waiters provides a few helpful APIs that make it very easy for us to indicate in our app (or addon) code that a given bit of async should be incorporated into a normal “settledness” check. The main building block that I would expect you to use here is waitFor (documentation here).

Using the decorator form of waitFor stacks quite nicely with @action and nicely absorbs all of the async going on in that action:

@action
@waitFor
async authenticate(e) {
  // ...snip...
}

Hope that helps!!

6 Likes

This isn’t quite correct, await click(...) internally does call await settled(). See prior reply for the underlying issue (basically the async done in the action wasn’t seen by the settledness system as something to wait for before continuing).

2 Likes

Thanks for the effort @rwjblue, i will try that ASAP, will post here.

Sorry for the late answer. The solution works as intended. Thanks for hints :wink:

Just came across this issue too. While explicitly using waitFor in app code does seem to work, it also doesn’t feel like the type of thing every app should have to concern itself with. Would it be a good idea for ember-simple-auth to be using waitFor (or something similar) itself to wait for async callbacks in any authenticator? Or is this something that should be handled in each authenticator? Or does it really need to be handled by each app individually?

Edit: filed an issue on the ember-simple-auth repo

One very important thing to call out here: we do not wait for “all promises to be resolved”! (Doing this is basically impossible anyways…)

Didn’t it used to work this way? Asking because I came across this post while investigating why our tests no longer wait for RSVP.Promise to resolve.

Edit: Never mind, I tested the example I provided it didn’t work the way I thought it did. I am wrong and am removing my bad example to avoid confusing others who might stumble on this post.

Edit 2: After some research, my misunderstanding was because I wrongly assumed RSVP.Promise was responsible for the tests tracking async actions and that is how andThen knew to wait for it to complete. I learned it was actually because the tests tracked jQuery.ajax requests and only when those were settled, did the tests know they could continue. Now @ember/test-waiters makes a lot more sense as a way to tell tests to wait for actions without requiring jQuery.