Testing async DOM functions in Ember.js - a custom waiter alternative

18 January 2024

In my last blog post, we wrote a custom test waiter to reliably test a component that relied on a DOM callback that wasn't integrated into Ember's testing framework.

In the end, we saw that the test also passed without the test waiter when we added an extra await settled() after the component was rendered.

This seemed counter-intuitive, and Ember even called us out on it via a lint error since await render() already calls settled internally.

This post discovers why this passes and offers an alternative to implementing a custom test waiter.

To harness the collective mind of the Ember ecosystem, I asked the question on Discuss. The responses confirmed my suspicion about why it passes, and I also received a few great ideas about alternatives.

Buying just a little bit of time

The test looks like this:

 1module('Integration | Component | avatar', function (hooks) {
 2  setupRenderingTest(hooks);
 3
 4  test('It falls back to initials when the image cannot be loaded', async function (assert) {
 5    await render(
 6      hbs`<Avatar @name="Marquis de Carabas" @url="/images/non-existent.webp" />`,
 7    );
 8    await settled();
 9    assert.dom('.initials').hasText('MC');
10  });
11});

The extra settled call takes very little time, but seems enough for the DOM function to complete in almost all cases (I re-ran the test about 50 times, and it passed every time).

So this fixes the test by a happy incident: Ember doesn't wait for the callback to complete, but the few milliseconds added by the extra settled is just enough for this, so the assertion finds the component in the expected state.

Short of implementing the waiter, can we do better?

Wait for it

One of the great things about Ember is that it takes testing seriously and thus allows us great tools to test our apps. Unsurprisingly, it has a test helper for this case called waitFor.

The docstring of waitFor tells a great deal about what we can use it for:

Used to wait for a particular selector to appear in the DOM. Due to the fact that it does not wait for general settledness, this is quite useful for testing interim DOM states (e.g. loading states, pending promises, etc).

Without the waiter, general settledness doesn't help us, so waitFor works great for our use case: we want to wait for the initials to appear so that we know that our fallback worked correctly:

 1module('Integration | Component | avatar', function (hooks) {
 2  setupRenderingTest(hooks);
 3
 4  test('It falls back to initials when the image cannot be loaded', async function (assert) {
 5    await render(
 6      hbs`<Avatar @name="Marquis de Carabas" @url="/images/non-existent.webp" />`,
 7    );
 8    await waitFor('.initials');
 9    assert.dom('.initials').hasText('MC');
10  });

This works great! The default timeout, 1000ms, is more than enough for the callback passed to onError to run and render the initials, so this makes the test pass (you can pass another timeout value if you specify a timeout parameter).

(To add another tool to your testing toolbelt: waitFor calls another waiter function, waitUntil, which is even more potent as you can wait for more things than the appearance of a selected DOM element)

Custom waiter vs. waitFor

It's important to note that the mechanism by which the extra settled() and waitFor() make the test pass is similar.

Although waitFor is specific about what it wants to see in the DOM, it doesn't guarantee that our implementation works: if it takes more than 1000 msecs for our onError callback plus the re-render to run, it will fail.

The custom waiter solution is more reliable: as Ember's testing framework doesn't execute the assertion before the waiter has finished, it guarantees there will be no false negatives (the test saying the implementation doesn't work when it does).

So, which one should you pick?

You probably won't like the answer, but I have to say: "it depends." :)

I like the actual guarantee that the test waiter brings, I think it's the "real", the most reliable solution.

On the other hand, it's certainly more ceremony than waitFor, and you have to add a non-trivial amount of code to the component "just" to test it (see the "Adding code to make something testable" section in the last post).

And if you don't have control over the code you test, you can't add a waiter to it, so your only option is waitFor.

waitFor is also great for testing interim DOM state (as the code comment says), like verifying that a toast message is shown. In those cases, you can't use a custom waiter because you don't want to wait for everything to settle.

So as not to avoid deciding on this one, I'd add a custom waiter, but I wouldn't hesitate for a moment to approve a PR that uses a waitFor.

Thanks to Dan and Boris for replying to my Discuss blog post. It served as the content for this blog post.

Share on Twitter