Testing Components

Components can be tested easily with a rendering test. Let's see how this plays out in a specific example:

Let's assume we have a component with a style property that is updated whenever the value of the name property changes. The style attribute of the component is bound to its style property.

You can follow along by generating your own component with ember generate component pretty-color.

import Component from '@glimmer/component';

export default class PrettyColorComponent extends Component {
  get style() {
    return `color: ${this.args.name}`;
  }
}
<div style={{this.style}}>
  Pretty Color: {{@name}}
</div>

The module from QUnit will scope your tests into groups of tests which can be configured and run independently. Make sure to call the setupRenderingTest function together with the hooks parameter first in your new module. This will do the necessary setup for testing your component for you, including setting up a way to access the rendered DOM of your component later on in the test, and cleaning up once your tests in this module are finished.

import { module } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

module('Integration | Component | pretty-color', function(hooks) {
  setupRenderingTest(hooks);

});

Inside of your module and after setting up the test, we can now start to create our first test case. Here, we can use the QUnit.test helper and we can give it a descriptive name:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

module('Integration | Component | pretty-color', function(hooks) {
  setupRenderingTest(hooks);

  test('should change colors', async function(assert) {


  });
});

Also note how the callback function passed to the test helper is marked with the keyword async. The ECMAScript 2017 feature async/await allows us to write asynchronous code in an easy-to-read, seemingly synchronous manner. We can better see what this means, once we start writing out our first test case:

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

module('Integration | Component | pretty-color', function(hooks) {
  setupRenderingTest(hooks);

  test('should change colors', async function(assert) {
    assert.expect(1);

    // set the outer context to red
    this.set('colorValue', 'red');

    await render(hbs`<PrettyColor @name={{this.colorValue}} />`);

    assert.equal(this.element.querySelector('div').getAttribute('style'), 'color: red', 'starts as red');
  });
});

Each test can use the render() function imported from the @ember/test-helpers package to create a new instance of the component by declaring the component in template syntax, as we would in our application. Also notice, the keyword await in front of the call to render. It allows the test which we marked as async earlier to wait for any asynchronous behavior to complete before executing the rest of the code below. In this case our first assertion will correctly execute after the component has fully rendered.

Next we can test that changing the component's name property updates the component's style attribute and is reflected in the rendered HTML:

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

module('Integration | Component | pretty-color', function(hooks) {
  setupRenderingTest(hooks);

  test('it renders', async function(assert) {
    assert.expect(2);

    // set the outer context to red
    this.set('colorValue', 'red');

    await render(hbs`<PrettyColor @name={{this.colorValue}} />`);

    assert.equal(this.element.querySelector('div').getAttribute('style'), 'color: red', 'starts as red');

    this.set('colorValue', 'blue');

    assert.equal(this.element.querySelector('div').getAttribute('style'), 'color: blue', 'updates to blue');  });
});

We might also test this component to ensure that the content of its template is being rendered properly:

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

module('Integration | Component | pretty-color', function(hooks) {
  setupRenderingTest(hooks);

  test('it renders', async function(assert) {
    assert.expect(2);

    this.set('colorValue', 'orange');

    await render(hbs`<PrettyColor @name={{this.colorValue}} />`);

    assert.equal(this.element.textContent.trim(), 'Pretty Color: orange', 'text starts as orange');

    this.set('colorValue', 'green');

    assert.equal(this.element.textContent.trim(), 'Pretty Color: green', 'text switches to green');
  });
});

Testing User Interaction

Components are a great way to create powerful, interactive, and self-contained custom HTML elements. It is important to test the component's methods and the user's interaction with the component.

Imagine you have the following component that changes its title when a button is clicked on:

You can follow along by generating your own component with ember generate component magic-title.

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

export default class MagicTitleComponent extends Component {
  title = 'Hello World';

  @action
  updateTitle() {
    this.set('title', 'This is Magic');
  }
}
<h2>{{this.title}}</h2>

<button type="button" class="title-button" {{on "click" this.updateTitle}}>
  Update Title
</button>

And our test might look like this:

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

module('Integration | Component | magic-title', function(hooks) {
  setupRenderingTest(hooks);

  test('should update title on button click', async function(assert) {
    assert.expect(2);

    await render(hbs`<MagicTitle />`);

    assert.equal(this.element.querySelector('h2').textContent.trim(), 'Hello World', 'initial text is hello world');

    //Click on the button
    await click('.title-button');

    assert.equal(this.element.querySelector('h2').textContent.trim(), 'This is Magic', 'title changes after click');
  });
});

Note how we make use of the click helper from ember-test-helpers to interact with the component DOM to trigger the updateTitle action. You can find many other helpful helpers for simulating user interaction in rendering tests in the API documentation of ember-test-helpers.

Testing Actions

Components starting in Ember 2 utilize closure actions. Closure actions allow components to directly invoke functions provided by outer components.

For example, imagine you have a comment form component that invokes a submitComment action when the form is submitted, passing along the form's data:

You can follow along by generating your own component with ember generate component comment-form.

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

export default class CommentFormComponent extends Component {
  @tracked comment = '';

  @action
  submitComment() {
    this.args.submitComment({ comment: this.comment });
  }
}
<form {{action "submitComment" on="submit"}}>
  <label for="comment">Comment:</label>
  <Textarea @id="comment" @value={{this.comment}} />
  <input class="comment-input" type="submit" value="Submit"/>
</form>

Here's an example test that asserts that the specified externalAction function is invoked when the component's internal submitComment action is triggered by making use of a test double (dummy function). assert.expect(1) at the top of the test makes sure that the assertion inside the external action is called:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, fillIn, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | comment-form', function(hooks) {
  setupRenderingTest(hooks);

  test('should trigger external action on form submit', async function(assert) {
    assert.expect(1);

    // test double for the external action
    this.set('externalAction', (actual) => {
      let expected = { comment: 'You are not a wizard!' };
      assert.deepEqual(actual, expected, 'submitted value is passed to external action');
    });

    await render(hbs`<CommentForm @submitComment={{action externalAction}} />`);

    // fill out the form and force an onchange
    await fillIn('textarea', 'You are not a wizard!');

    // click the button to submit the form
    await click('.comment-input');
  });
});

Stubbing Services

In cases where components have dependencies on Ember services, it is possible to stub these dependencies for rendering tests. You stub Ember services by using the built-in register() function to register your stub service in place of the default.

Imagine you have the following component that uses a location service to display the city and country of your current location:

You can follow along by generating your own component with ember generate component location-indicator.

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class LocationIndicatorComponent extends Component {
  @service location;

  // when the coordinates change, call the location service to get the current city and country
  get city() {
    return this.location.getCurrentCity();
  }

  get country() {
    return this.location.getCurrentCountry();
  }
}
You currently are located in {{this.city}}, {{this.country}}

To stub the location service in your test, create a local stub object that extends Service from @ember/service, and register the stub as the service your tests need in the beforeEach function. In this case we initially force location to "New York".

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Service from '@ember/service';

//Stub location service
class LocationStub extends Service {
  city = 'New York';
  country = 'USA';
  currentLocation = {
    x: 1234,
    y: 5678
  };

  getCurrentCity() {
    return this.city;
  }

  getCurrentCountry() {
    return this.country;
  }
}

module('Integration | Component | location-indicator', function(hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function(assert) {
    this.owner.register('service:location-service', LocationStub);
  });
});

Once the stub service is registered, the test needs to check that the stub data from the service is reflected in the component output.

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Service from '@ember/service';

//Stub location service
class LocationStub extends Service {
  city = 'New York';
  country = 'USA';
  currentLocation = {
    x: 1234,
    y: 5678
  };

  getCurrentCity() {
    return this.city;
  }

  getCurrentCountry() {
    return this.country;
  }
}

module('Integration | Component | location-indicator', function(hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function(assert) {
    this.owner.register('service:location-service', LocationStub);
  });

  test('should reveal current location', async function(assert) {
    await render(hbs`<LocationIndicator />`);
    assert.equal(this.element.textContent.trim(),
     'You currently are located in New York, USA');
  });
});

In the next example, we'll add another test that validates that the display changes when we modify the values on the service.

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Service from '@ember/service';

//Stub location service
class LocationStub extends Service {
  city = 'New York';
  country = 'USA';
  currentLocation = {
    x: 1234,
    y: 5678
  };

  getCurrentCity() {
    return this.city;
  }

  getCurrentCountry() {
    return this.country;
  }
}

module('Integration | Component | location-indicator', function(hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function(assert) {
    this.owner.register('service:location-service', LocationStub);
  });

  test('should reveal current location', async function(assert) {
    await render(hbs`<LocationIndicator />`);
    assert.equal(this.element.textContent.trim(),
     'You currently are located in New York, USA');
  });

  test('should change displayed location when current location changes', async function (assert) {
    await render(hbs`<LocationIndicator />`);

    assert.equal(this.element.textContent.trim(),
     'You currently are located in New York, USA', 'origin location should display');

    this.locationService = this.owner.lookup('service:location-service');
    this.set('locationService.city', 'Beijing');
    this.set('locationService.country', 'China');
    this.set('locationService.currentLocation', { x: 11111, y: 222222 });

    assert.equal(this.element.textContent.trim(),
     'You currently are located in Beijing, China', 'location display should change');
  });
});

Waiting on Asynchronous Behavior

Often, interacting with a component will cause asynchronous behavior to occur, such as HTTP requests, or timers. The module @ember/test-helpers provides you with several useful helpers that will allow you to wait for any asynchronous behavior to complete that is triggered by a DOM interaction induced by those. To use them in your tests, you can await any of them to make sure that subsequent assertions are executed once the asynchronous behavior has fully settled:

await click('button.submit-button'); // clicks a button and waits for any async behavior initiated by the click to settle
assert.equal(this.element.querySelector('.form-message').textContent, 'Your details have been submitted successfully.');

Nearly all of the helpers for DOM interaction from @ember/test-helpers return a call to settled - a function that ensures that any Promises, operations in Ember's run loop, timers or network requests have already resolved. The settled function itself returns a Promise that resolves once all async operations have come to an end.

You can use settled as a helper in your tests directly and await it for all async behavior to settle deliberately.

Imagine you have a typeahead component that uses Ember.run.debounce to limit requests to the server, and you want to verify that results are displayed after typing a character.

You can follow along by generating your own component with ember generate component delayed-typeahead.

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { debounce } from '@ember/runloop';

export default class DelayedTypeaheadComponent extends Component {
  @action
  handleTyping() {
    //the fetchResults function is passed into the component from its parent
    debounce(this, this.fetchResults, this.searchValue, 250);
  }
};
<label for="search">Search</label>
<Input @id="search" @value={{this.searchValue}} @key-up={{this.handleTyping}} />
<ul>
  {{#each this.results as |result|}}
    <li class="result">{{result.name}}</li>
  {{/each}}
</ul>

In your test, use the settled helper to wait until your debounce timer is up and then assert that the page is rendered appropriately.

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

module('Integration | Component | delayed-typeahead', function(hooks) {
  setupRenderingTest(hooks);

  const stubResults = [
    { name: 'result 1' },
    { name: 'result 2' }
  ];

  test('should render results after typing a term', async function(assert) {
    assert.expect(2);

    this.set('results', []);
    this.set('fetchResults', (value) => {
      assert.equal(value, 'test', 'fetch closure action called with search value');
      this.set('results', stubResults);
    });

    await render(hbs`<DelayedTypeahead @fetchResults={{this.fetchResults}} @results={{this.results}} />`);
    this.element.querySelector('input').value = 'test';
    this.element.querySelector('input').dispatchEvent(new Event('keyup'));

    await settled();

    assert.equal(this.element.querySelectorAll('.result').length, 2, 'two results rendered');
  });
});

© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/testing/testing-components