Skip to content

Testing: Creating Component Tests

Jonathan Niles edited this page Mar 4, 2016 · 3 revisions

Components remove responsibility from the controller/page by managing their own dependencies, state, and view. In the same way, each component should be wrapped in a test harness (in the test/e2e/components directory) that removes responsibility from the test script. This lets a developer write logical tests, rather than lots of boilerplate code to interact with each component.

As an example, we will be building a fictional bhPatientCard component to display a patient's medical information in a consistent manner. The HTML signature will look like this:

<bh-patient-card patient-id="SomeController.patientId"></bh-patient-card>

Namespacing the component

The first step to adding testing to a component is to make a namespace tag for it. Inside the template for the bhPatientCard component, we will put a top-level data-* attribute. Using a data-* attributes is recommended since they allow detailed descriptions, allow assignment (e.g data-example-option="1", and can be repeated multiple times in the HTML page.

<!-- mark the top level element-->
<div data-bh-patient-card>
  <!-- contents of the patient card -->
</div>

You should never use an id attribute as a namespace - placing more than one component on the page will violate the HTML spec. See this reference for more details.

Creating a test harness

Once the component has been namespaced, it can easily be found on a page. If more than one instance of the component exists on a page, it can be uniquely identified by the parent controller via an id attribute. For example, suppose we have the following situation:

<div class="patient-list">
  <bh-patient-card ng-repeat="patient in ParentController" id="card-{{ patient.id }}" patient-id="patient.id">
  </bh-patient-card>
</div>

Since we used a data-* attribute to namespace the client, this does not violate the HTML spec's id attribute rules.

The test harness should include the namespace as a selector.

// inside test/e2e/shared/components/bhPatientCard.js
module.exports = {
  selector : '[data-bh-patient-card]',

  /* harness methods ... */

};

Adding methods to the harness

Let's assume that the bhPatientCard has a button to click on it, which will close the card. We'll make a wrapper method to perform this action, called close(). Let's look at the HTML template:

<div data-bh-patient-card>

  <!--
   ... patient information displayed in a logical fashion ...
  -->

  <button class="btn btn-default" data-action="close">
    Close
  </button>
</div>

So, we need a method to locate the button, and click it.

module.exports = {
  selector : '[data-bh-patient-card]',

  // this method closes the patient card
  close : function close() {
  
    // locate the patient card on the page
    var card = element(by.css(this.selector));
  
    // locate the <button> inside the patient card
    var btn = card.element(by.css('[data-action="close"]'));

    // click the button!
    btn.click();
  }
};

Now, in our test, we can use the wrapper like this:

var bhPatientCard = require('path/to/bhPatientCard.js');

describe('Some Controller', function () {

  it('Can close the patient card', function () {
    bhPatientCard.close();
  });

});

Using ids to identify components

The close() method works well for a single component, but what happens if we have more than one <bh-patient-card> on the page? The close() method could take in an id, and use that to locate and close the correct card. Let's modify our method:

module.exports = {
  selector : '[data-bh-patient-card]',

  // this method closes the patient card.  If an id is provided, it will locate the card with that id
  close : function close(id) {
  
    // locate the patient card on the page, using an id if it exits.
    var card = element(id ? by.id(id)  : by.css(this.selector));
  
    // locate the <button> inside the patient card
    var btn = card.element(by.css('[data-action="close"]'));

    // click the button!
    btn.click();
  }
};

Now we can write the following test:

var bhPatientCard = require('path/to/bhPatientCard.js');

describe('Some Controller', function () {

  it('Can close the correct patient card', function () {
  
    // assert the card exists
    expect(element(by.id('card-3')).isPresent()).to.eventually.be.true;
 
    // close card with id = 'card-3'
    bhPatientCard.close('card-3');

    // assert the card has been removed
    expect(element(by.id('card-3')).isPresent()).to.eventually.be.false;
  });

  it('Can still close a patient card without an id', function () {

    // in this case, we expect only one patient card to be on the page.
    expect(element(by.css(bhPatientCard.selector)).isPresent()).to.eventually.be.true;

    // close the only patient card on the page
    bhPatientCard.close();

    // make sure there are no more patient cards on the page.
    expect(element(by.css(bhPatientCard.selector)).isPresent()).to.eventually.be.false;
  });
});