





















































In this article by Simon Bailey, the author of AngularJS Testing Cookbook, we will cover the following recipes:
(For more resources related to this topic, see here.)
Directives are the cornerstone of AngularJS and can range in complexity providing the foundation to many aspects of an application. Therefore, directives require comprehensive tests to ensure they are interacting with the DOM as intended. This article will guide you through some of the rudimentary steps required to embark on your journey to test directives. The focal point of many of the recipes revolves around targeting specific HTML elements and how they respond to interaction. You will learn how to test changes on scope based on a range of influences and finally begin addressing testing directives using Protractor.
Testing a directive involves three key steps that we will address in this recipe to serve as a foundation for the duration of this article:
For this recipe, you simply need a directive that applies a scope value to the element in the DOM. For example:
angular.module('chapter5', []) .directive('writers', function() { return { restrict: 'E', link: function(scope, element) { element.text('Graffiti artist: ' + scope.artist); } }; });
beforeEach(module('chapter5'));
beforeEach(inject(function ($rootScope, $compile) { scope = $rootScope.$new(); scope.artist = artist; }));
element = angular.element('<writers></writers>');
$compile(element)(scope);
scope.$digest();
it('should display correct text in the DOM', function() { expect(element.text()).toBe('Graffiti artist: ' +
artist); });
Here is what your code should look like to run the final test:
var scope; var element; var artist; beforeEach(module('chapter5')); beforeEach(function() { artist = 'Amara Por Dios'; }); beforeEach(inject(function($compile) { element = angular.element('<writers></writers>'); scope.artist = artist; $compile(element)(scope); scope.$digest(); })); it('should display correct text in the DOM', function() { expect(element.text()).toBe('Graffiti artist: ' +
artist); });
In step 4, the directive HTML tag is provided as a string to the angular.element function. The angular element function wraps a raw DOM element or an HTML string as a jQuery element if jQuery is available; otherwise, it defaults to using Angular's jQuery lite which is a subset of jQuery. This wrapper exposes a range of useful jQuery methods to interact with the element and its content (for a full list of methods available, visit https://docs.angularjs.org/api/ng/function/angular.element).
In step 6, the element is compiled into a template using the $compile service. The $compile service can compile HTML strings into a template and produces a template function. This function can then be used to link the scope and the template together. Step 6 demonstrates just this, linking the scope object created in step 3. The final step to getting our directive in a testable state is in step 7 where we call $digest to simulate the scope life cycle. This is usually part of the AngularJS life cycle within the browser and therefore needs to be explicitly called in a test-based environment such as this, as opposed to end-to-end tests using Protractor.
One beforeEach() method containing the logic covered in this recipe can be used as a reference to work from for the rest of this article:
beforeEach(inject(function($rootScope, $compile) { // Create scope scope = $rootScope.$new(); // Replace with the appropriate HTML string element = angular.element('<deejay></deejay>'); // Replace with test scope data scope.deejay = deejay; // Compile $compile(element)(scope); // Digest scope.$digest(); }));
It's fairly common to separate the template content into an HTML file that can then be requested on demand when the directive is invoked using the templateUrl property. However, when testing directives that make use of the templateUrl property, we need to load and preprocess the HTML files to AngularJS templates. Luckily, the AngularJS team preempted our dilemma and provided a solution using Karma and the karma-ng-html2js-preprocessor plugin. This recipe will show you how to use Karma to enable us to test a directive that uses the templateUrl property.
For this recipe, you will need to ensure the following:
angular.module('chapter5', []) .directive('emcees', function() { return { restrict: 'E', templateUrl: 'template.html', link: function(scope, element) { scope.emcee = scope.emcees[0]; } }; })
An example template could be as simple as what we will use for this example (template.html):
<h1>{{emcee}}</h1>
beforeEach(module('chapter5'));
beforeEach(module('chapter5', 'template.html'));
beforeEach(inject(function ($rootScope, $compile) { scope = $rootScope.$new(); Scope.emcees = emcees; }));
element = angular.element('<emcees></emcees>');
$compile(element)(scope);
scope.$digest();
it('should set the scope property id to the correct initial
value', function () {});
var h1 = element.find('h1');
expect(h1.text()).toBe(emcees[0]);
The karma-ng-html2js-preprocessor plugin works by converting HTML files into JS strings and generates AngularJS modules that we load in step 3. Once loaded, AngularJS makes these modules available by putting the HTML files into the $templateCache. There are libraries available to help incorporate this into your project build process, for example using Grunt or Gulp. There is a popular example specifically for Gulp at https://github.com/miickel/gulp-angular-templatecache. Now that the template is available, we can access the HTML content using the compiled element we created in step 5.
In this recipe, we access the text content of the element using the find() method. Be aware that if using the smaller jQuery lite subset of jQuery, there are certain limitations compared to the full-blown jQuery version. The find() method in particular is limited to look up by tag name only. To read more about the find() method, visit the jQuery API documentation at http://api.jquery.com/find.
Directives, as you should know, attach special behavior to a DOM element. When AngularJS compiles and returns the element on which the directive is applied, it is wrapped by either jqLite or jQuery. This exposes an API on the element, offering many useful methods to query the element and its contents. In this recipe, you will learn how to use these methods to retrieve elements using selectors.
Follow the logic to define a beforeEach() function with the relevant logic to set up a directive as outlined in the Starting with testing directives recipe in this article. For this recipe, you can replicate the template that I suggested in the first recipe's There's more… section. For the purpose of this recipe, I tested against a property on scope named deejay:
var deejay = { name: 'Shortee', style: 'turntablism' };
You can replace this with whatever code you have within the directive you're testing.
it('should return an element using find()', function ()
{});
var h2 = element.find('h2');
expect(h2[0]).toBeDefined();
In step 2, we use the find() method with the h2 selector to test against in step 3's expectation. Remember, the element returned is wrapped by jqLite or jQuery. Therefore, even if the element is not found, the object returned will have jQuery-specific properties; this means that we cannot run an expectation on the element alone being defined. A simple way to determine if the element itself is indeed defined is to access it via jQuery's internal array of DOM objects, typically the first. So, this is why in our recipe we run an expectation against element[0] as opposed to element itself.
it('should return an element using querySelector and css
selector', function() { var elementByClass = element[0].querySelector('.deejay-
style'); expect(elementByClass).toBeDefined(); });
it(should return an element using querySelector and id selector', function() { var elementByClass = element[0].querySelector(' #deejay_name'); expect(elementByClass).toBeDefined(); });
You can read more about the querySelector() method at https://developer.mozilla.org/en-US/docs/Web/API/document.querySelector.
A substantial number of directive tests will involve interacting with the HTML content within the rendered HTML template. This recipe will teach you how to test whether a directive's HTML content is as expected.
Follow the logic to define a beforeEach() function with the relevant logic to set up a directive as outlined in the Starting with testing directives recipe in this article. For this recipe, you can replicate the template that I suggested in the first recipe's There's more… section. For the purpose of this recipe, I will test against a property on a scope named deejay:
var deejay = { name: 'Shortee', style: 'turntablism' };
You can replace this with whatever code you have within the directive you're testing.
it('should display correct deejay data in the DOM',
function () {});
var h2 = element.find('h2');
expect(h2.html()).toBe(deejay.name);
We made heavy use of the jQuery (or jqLite) library methods available for our element. In step 2, we use the find() method with the h2 selector. This returns a match for us to further utilize in step 3, in our expectation where we access the HTML contents of the element using the html() method this time (http://api.jquery.com/html/).
We could also run a similar expectation for text within our h2 element using the text() method (http://api.jquery.com/text/) on the element, for example:
it('should retrieve text from <h2>', function() { var h2 = element.find('h2'); expect(h2.text()).toBe(deejay.name); });
AngularJS facilitates generating repeated content with ease using the ngRepeat directive. In this recipe, we'll learn how to access and test repeated content.
Follow the logic to define a beforeEach() function with the relevant logic to set up a directive as outlined in the Starting with testing directives recipe in this article. For this recipe, you can replicate the template that I suggested in the first recipe's There's more… section. For the purpose of this recipe, I tested against a property on scope named breakers:
var breakers = [{ name: 'China Doll' }, { name: 'Crazy Legs' }, { name: 'Frosty Freeze' }];
You can replace this with whatever code you have within the directive you're testing.
it('should display the correct breaker name', function ()
{});
var list = element.find('li');
expect(list.eq(0).text()).toBe('China Doll');
In step 2, the find() method using li as the selector will return all the list items. In step 3, using the eq() method (http://api.jquery.com/eq/) on the returned element from step 2, we can get the HTML contents at a specific index, zero in this particular case. As the returned object from the eq() method is a jQuery object, we can call the text() method, which immediately after that will return the text content of the element. We can then run an expectation that the first li tag text matches the first breaker within the scope array.
In this article you have learned to focus on testing changes within a directive based on interaction from either UI events or application updates to the model. Directives are one of the important jewels of AngularJS and can range in complexity. They can provide the foundation to many aspects of the application and therefore require comprehensive tests.
Further resources on this subject: