Testing Directives

Directives in AngularJS are responsible for encapsulating complex functionality within custom HTML tags, attributes, classes or comments. Unit tests are very important for directives because the components you create with directives may be used throughout your application and in many different contexts.

Simple HTML Element Directive

Let’s start with an AngularJS app with no dependencies.

var app = angular.module(‘myApp’, []);

Now we can add a directive to our app.

app.directive(‘aGreatEye’, function () {

return {

restrict: ‘E’,

replace: true,

template: ‘<h1>lidless, wreathed in flame, {{1 + 1}} times</h1>’

};

});

This directive is used as a tag <a-great-eye></a-great-eye>. It replaces the entire tag with the template <h1>lidless, wreathed in flame, {{1 + 1}} times</h1>. Now we are going to write a jasmine unit test to verify this functionality. Note that the expression {{1 + 1}} times will also be evaluated in the rendered content.

describe(‘Unit testing great quotes’, function() {

var $compile,

$rootScope;

// Load the myApp module, which contains the directive

beforeEach(module(‘myApp’));

// Store references to $rootScope and $compile

// so they are available to all tests in this describe block

beforeEach(inject(function(_$compile_, _$rootScope_){

// The injector unwraps the underscores (_) from around the parameter names when matching

$compile = _$compile_;

$rootScope = _$rootScope_;

}));

it(‘Replaces the element with the appropriate content’, function() {

// Compile a piece of HTML containing the directive

var element = $compile(“<a-great-eye></a-great-eye>”)($rootScope);

// fire all the watches, so the scope expression {{1 + 1}} will be evaluated

$rootScope.$digest();

// Check that the compiled element contains the templated content

expect(element.html()).toContain(“lidless, wreathed in flame, 2 times”);

});

});

We inject the $compile service and $rootScope before each jasmine test. The $compile service is used to render the aGreatEye directive. After rendering the directive we ensure that the directive has replaced the content and “lidless, wreathed in flame, 2 times” is present.

Underscore notation: The use of the underscore notation (e.g.: _$rootScope_) is a convention wide spread in AngularJS community to keep the variable names clean in your tests. That’s why the $injector strips out the leading and the trailing underscores when matching the parameters. The underscore rule applies only if the name starts and ends with exactly one underscore, otherwise no replacing happens.

Testing Transclusion Directives

Directives that use transclusion are treated specially by the compiler. Before their compile function is called, the contents of the directive’s element are removed from the element and provided via a transclusion function. The directive’s template is then appended to the directive’s element, to which it can then insert the transcluded content into its template.

Before compilation:

<div transclude-directive>

Some transcluded content

</div>

After transclusion extraction:

<div transclude-directive></div>

After compilation:

<div transclude-directive>

Some Template

<span ng-transclude>Some transcluded content</span>

</div>

If the directive is using ‘element’ transclusion, the compiler will actually remove the directive’s entire element from the DOM and replace it with a comment node. The compiler then inserts the directive’s template “after” this comment node, as a sibling.

Before compilation

<div element-transclude>

Some Content

</div>

After transclusion extraction

<!– elementTransclude –>

After compilation:

<!– elementTransclude –>

<div element-transclude>

Some Template

<span ng-transclude>Some transcluded content</span>

</div>

It is important to be aware of this when writing tests for directives that use ‘element’ transclusion. If you place the directive on the root element of the DOM fragment that you pass to $compile, then the DOM node returned from the linking function will be the comment node and you will lose the ability to access the template and transcluded content.

var node = $compile(‘<div element-transclude></div>’)($rootScope);

expect(node[0].nodeType).toEqual(node.COMMENT_NODE);

expect(node[1]).toBeUndefined();

To cope with this you simply ensure that your ‘element’ transclude directive is wrapped in an element, such as a <div>.

var node = $compile(‘<div><div element-transclude></div></div>’)($rootScope);

var contents = node.contents();

expect(contents[0].nodeType).toEqual(node.COMMENT_NODE);

expect(contents[1].nodeType).toEqual(node.ELEMENT_NODE);

Testing Directives With External Templates

If your directive uses templateUrl, consider using karma-ng-html2js-preprocessor to pre-compile HTML templates and thus avoid having to load them over HTTP during test execution. Otherwise you may run into issues if the test directory hierarchy differs from the application’s.

Get industry recognized certification – Contact us

Menu