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;
});
});