Why does `settled` make a test pass after `render`?

Howdy,

I wrote a blog post about how to add a custom waiter in a component that uses an async DOM function to be able to test it properly.

Here is the component:

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class AvatarComponent extends Component {
  @tracked isShowingInitials = false;

  get initials() {
    const [first, ...rest] = this.args.name.split(/\s+/);
    const last = rest.pop();
    return [first, last].map((name) => name[0]).join('');
  }

  @action
  showInitials() {
    this.isShowingInitials = true;
  }
}

And its template:

<div class="avatar">
  {{#if this.isShowingInitials}}
    <div class="initials">{{this.initials}}</div>
  {{else}}
    <img
      src={{@url}}
      onError={{this.showInitials}}
      alt={{concat @name "'s avatar"}}
    />
  {{/if}}
  <div class="name">{{@name}}</div>
</div>

Here is the test:

module('Integration | Component | avatar', function (hooks) {
  setupRenderingTest(hooks);

  test('It falls back to initials when the image cannot be loaded', async function (assert) {
    await render(
      hbs`<Avatar @name="Marquis de Carabas" @url="/images/non-existent.webp" />`,
    );
    assert.dom('.initials').hasText('MC');
  });
});

If the waiter is not there, Ember doesn’t wait for the onError callback to finish before moving on to the assertion in the test, and thus the test fails. With the waiter properly set up, it works great.

However, I also noticed that if there is no custom waiter, but I add an await settled() after the await render() line in the test, the test seems to pass reliably:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'image-onerror/tests/helpers';
import { render, settled } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | avatar', function (hooks) {
  setupRenderingTest(hooks);

  test('It falls back to initials when the image cannot be loaded', async function (assert) {
    await render(
      hbs`<Avatar @name="Marquis de Carabas" @url="/images/non-existent.webp" />`,
    );
    await settled();
    assert.dom('.initials').hasText('MC');
  });
});

And this is what I’m quite baffled about. Adding the call to settled shouldn’t matter at all, it even prompts a linting error that says that I shouldn’t add a settled when the helper (the render) has it as its return value and yet with this addition the test seems to always pass.

So why does settled work here?

Is it that calling settled takes a minuscule amount of time but enough for the onError callback to run? Is it something else?

Thank you!

we’ve been seeing this too after “fixing” the lint errors. I haven’t done a deep dive as to why exactly this is in some cases.

One slightly better “fix” is to use waitFor to wait for the element that you need to assert against:

    await render(
      hbs`<Avatar @name="Marquis de Carabas" @url="/images/non-existent.webp" />`,
    );
    await waitFor('.initials');
    assert.dom('.initials').hasText('MC');
2 Likes

I believe that’s the reason, yes. As you mention yourself, settled is not “clever” enough to know about external promises/DOM callbacks so it’s just a race-condition. The test could theoretically fail, just less likely. I would do as @dknutsen said - use waitFor. It’s a wonderful tool!

2 Likes