Testing interim states with the latest test-helpers

I’m sure it used to be possible to do something like this:

let promise = fillIn('.some-field', 'Some value');

assert.ok(someInterimState);

await promise;

// ...

But all the helpers in @ember/test-helpers appear to be wrapped in a nextTickPromise so nothing happens synchronously. Is there any way to achieve the above these days?

You would use waitFor or waitUntil for this. Those helpers intentionally do not wait for “settledness” before continuing.

Here is an example test in @ember/test-helpers that shows what I mean:

  test('can check element while waiting for settled state', async function(assert) {
    let deferred = defer();
    this.set('promise', deferred.promise);

    // Does not use `await` intentionally
    let renderPromise = render(hbs`{{promise-wrapper promise=promise }}`);

    await waitFor('.loading');

    assert.equal(this.element.textContent, 'Please wait', 'has pending content');

    deferred.resolve('Yippie!');

    await renderPromise;

    assert.equal(this.element.textContent, 'Yippie!', 'has fulfillment value');
  });
1 Like

Simply beautiful :heart:

Thanks @rwjblue!

I just pushed a proposal to ember-cli-mirage to better support that use cases: Proposal: defer requests to improve testing pending states · Issue #35 · miragejs/discuss · GitHub Basically it let you defer a response after all tests of pending states are done. Would appreciate any feedback.

By the way waitFor and waitUntil test helpers are awesome!

Instead of creating a new duplicate topic, decide to revive this one.

I cannot catch a loading state in my component integration test. waitFor and waitUntil are called after the timeout finishes while I expect to get them called immediately on component render before the timed-out promise resolves.

Here is the component itself:

// app/components/branch-row.gts

export default class BranchRow extends Component<BranchRowSignature> {
  @service declare dfNotifications: DFNotificationsService;
  @service declare github: GithubService;

  @tracked branches: Branch[] = [];

  get show(): boolean {
    console.log('show', this.args);
    return (
      this.args.selectedRepo === this.args.repo?.name ||
      this.getRepoBranches.isRunning
    );
  }

  loadBranches = modifier(async () => {
    if (!this.branches.length)
      await this.getRepoBranches.perform(this.args.repo.url);
  });

  getRepoBranches = task({ drop: true }, async (url: string) => {
    try {
      const response = await this.github.getRepoBranches(url);

      if (response.ok) {
        this.branches = (await response.json()) as Branch[];
      } else {
        const failedResponse = (await response.json()) as NotFoundResponse;
        this.dfNotifications.notifyError(
          `${failedResponse.message}: couldn't load branches`,
        );
      }
    } catch (error: unknown) {
      if (error instanceof Error) {
        this.dfNotifications.notifyError(
          `${error.message}: couldn't load branches`,
        );
      }
      console.error(error);
    }
  });

  <template>
    {{#if this.show}}
      <tr
        class="branch-row"
        data-test-row-branch={{@repo.name}}
        {{this.loadBranches}}
      >
        <td class="df-cell" colspan="4">
          {{#if this.branches.length}}
            <h6 class="branch-heading">{{this.branches.length}} Branches</h6>
          {{/if}}
          <p class="branch-paragraph">
            {{#if this.getRepoBranches.isRunning}}
              ⏳ Loading...
            {{else}}
              {{#each this.branches as |branch|}}
                <span class="branch-name" title={{branch.name}}>
                  {{branch.name}}
                </span>
              {{else}}
                "No branches returned 🫗"
              {{/each}}
            {{/if}}
          </p>
        </td>
      </tr>
    {{/if}}
  </template>
}

and integration test:

tests/integration/components/branch-row-test.gts

test('it renders', async function (assert) {
    class StubGithubService extends Service {
      getRepoBranches() {
        return new Promise((resolve) => {
          later(
            () =>
              resolve(
                new Response(
                  JSON.stringify([
                    { name: 'main' },
                    { name: 'dev' },
                    { name: 'feature' },
                  ]),
                  {
                    status: 200,
                    headers: { 'Content-Type': 'application/json' },
                  },
                ),
              ),
            5000,
          );
        });
      }
    }
    this.owner.register('service:github', StubGithubService);
    class TestContext {
      @tracked repo: Repository = {
        name: 'ember',
        url: 'https://api.github.com/repos/emberjs/ember.js',
      };
    }

    const context = new TestContext();
    await render(
      <template>
        <BranchRow @selectedRepo="ember" @repo={{context.repo}} />
      </template>,
    );
    // await waitFor('[data-test-row-branch="ember"]');
    // await this.pauseTest();
    // await waitUntil(() => {
    assert.dom('[data-test-row-branch="ember"]').hasText('⏳ Loading...');
    // });
    await settled();
    // await waitFor('[data-test-row-branch="ember"]');
    assert
      .dom('[data-test-row-branch="ember"]')
      .hasText('3 Branches main dev feature');
    // .hasText('"No branches returned 🫗"');
  });

Prerequisites:

  ember -v
    ember-cli: 6.2.2
    node: 23.8.0
    os: darwin arm64

Do you have any suggestions on how to simulate the loading state in a test properly and how to test it?

Other things to mention:

  • using later lint errors on me with ember/no-runloop ember/no-runloop
  • this.set() global errors during the test run, so for the component to track the latest data, I resorted to using class TestContext with @tracked

I am open to reworking the component’s design and logic if that helps resolve my issue.

I have Monday brain right now so take this with a healthy dose of skepticism but I think you may not want to await render because I think that will wait for the initial render and anything trigged by it (e.g. loadBranches) to settle before moving on.

-    await render(
+   render(

Thank you, @dknutsen!

Indeed, dropping await solved the issue. As I figured, in my case the await was waiting for data load to be finished, which in my component design was triggered on an initial component render. Although I had to add two other tweaks (thanks to replies in Discord forum topic):

  1. void render( due to aggressive lint settings about forcing awaits on promises, and
  2. adding the line below await renderSettled();

The resulting test:

import { render, settled } from '@ember/test-helpers';
import { renderSettled } from '@ember/renderer';
...
test('it renders', async function (assert) {
    class StubGithubService extends Service {
      getRepoBranches() {
        return new Promise((resolve) => {
          resolve(
            new Response(
              JSON.stringify([
                { name: 'main' },
                { name: 'dev' },
                { name: 'feature' },
              ]),
              {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
              },
            ),
          );
        });
      }
    }
    this.owner.register('service:github', StubGithubService);
    const repo: Repository = {
      name: 'ember',
      url: 'https://api.github.com/repos/emberjs/ember.js',
    };

    void render(
      <template><BranchRow @selectedRepo="ember" @repo={{repo}} /></template>,
    );

    await renderSettled();
    assert
      .dom('[data-test-branches-row="ember"]')
      .hasText(
        '⏳ Loading...',
        'should show loading copy on initial render before data is loaded',
      );

    await settled();
    assert
      .dom('[data-test-branches-row="ember"]')
      .hasText(
        '3 Branches main dev feature',
        'should show branches list once data is loaded',
      );
  });

Ah that’s a perfect use case for the renderSettled helper too, nice! Glad you got it working!

1 Like