A. Sharif

Testing AngularJS hierarchical Directives with Jasmine

23 Jun 2014

The Basics

Based on the previous Testing AngularJS Directives with Jasmine post we will extend the collection directive. The collection directive renders a set of rows based on a given data set. In this example the existing directive will call another directive, the item directive for every data object. Before we continue, you might have a look at the original code.

Testing hierarchical Directives

Take a look at the adapted collection example, we are calling the item directive on every ng-repeat .

Module.directive('collection', function () {
    return {
        restrict: 'EA',
        transclude: true,
        replace: true,
        scope: {
            items: '=items'
        },
        controller: 'collectionController',
        template: '<div class="table table">' +
            '<div ng-transclude></div>' +
            '<div ng-repeat="item in items">' +
            '<item data-item="item"></item>' +
            '</div>' +
            '<div><button ng-click="addNewItem()">Add new Item</button></div>' +
            '</div>'
    };
});

The item directive is very minimal, all it does is render the item itself.

Module.directive('item', function () {
    return {
        require: '^collection',
        restrict: 'E',
        scope: {
            item: '='
        },
        replace: true,
        link: function ($scope, $element, $attrs, $tabsCtrl) {
            $scope.selectItem = $tabsCtrl.selectItem;
        },
        template:   '<div class="item-class" ng-class="{active: item.selected}"> - id: ' +
                    ' <button class="btn" ng-disabled="item.selected" ng-click="selectItem(item)">Set Active</button>' +
                    '</div>'
    };
});

But what if we do not want to test both directives? Can we test the item directive isolated from the collection directive?

The best approach is probably to mock the low level directive with $compileProvider.

Setting the directive to a very high priority and terminal to true will ensure that the mocked directive will be called while the real directive will never be executed.

beforeEach(module('example', function($compileProvider) {
    $compileProvider.directive('item', function() {
      var fake = {
        priority: 100,
        terminal: true,
        restrict: 'E',
        template: '<div class="fake">Not the real thing.</div>',
      };
      return fake;
    });
}));

Testing the directive:

it('should create clickable items', function() {
    var items = elm.find('.fake');
    expect(items.length).toBe(2);
});

Other options include mocking low level directives via a directive factory or overriding via higher priority and terminal true like in the following example (which is probably the worst approach and has absolutely no advantage compared to the first approach)

beforeEach(function() {
    angular.module('example').directive('item', function() {
        return {
             priority: 100,
            terminal: true,
            template : '<div class="fake">Not the real thing.</div>'
        }
    });
});

How to mock another directive's controller

In our example the item directive depends on the collection controller

Module.directive('item', function () {
    return {
        require: '^collection',
        restrict: 'E',
        // etc...

There a couple of possible approaches here:

One way is to wrap the collection directive around the item directive when calling $compile the other approach is to mock the controller. Adding a jasmine spy will also enable to verify if certain methods have been called when triggering a click for example. Here is an example implementation for a mocked controller:

beforeEach(inject(function($rootScope, $compile) {
    elm = angular.element(
      '<div>' +
      '<item data-item="dataset"></item>' +
      '</div>');

    scope = $rootScope.$new();

    var collectionController = {
      setItem: function() {}
    };

    scope.dataset = {id:1, title : 'testing', selected : false};
    elm.data('$collectionController', collectionController);

    $compile(elm)(scope);
    scope.$digest();
}));

it('should create one div item', function() {
    var items = elm.find('.item-class');
    expect(items.text()).toContain('testing - id: 1');
});

Links

AngularJS Directives Documentation

Jasmine

Karma

ngDirective Testing Example