Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I inject a mock dependency into an angular directive with Jasmine on Karma

I have the following directive:

function TopLevelMenuDirective ($userDetails, $configuration) {
    return {
        restrict:'A',
        templateUrl: staticFilesUri + 'templates/TopLevelMenu.Template.html', 
        scope: {
            activeTab: '='
        },
        link: function (scope, element, attributes) {
            var userDetails = $userDetails;
            if ($userDetails) {
                scope.user = {
                    name: userDetails.name ? userDetails.name : 'KoBoForm User',
                    avatar: userDetails.gravatar ? userDetails.gravatar: (staticFilesUri + '/img/avatars/example-photo.jpg')
                };
            } else {
                scope.user = {
                    name: 'KoBoForm User',
                    avatar: staticFilesUri + '/img/avatars/example-photo.jpg'
                }
            }

            scope.sections = $configuration.sections();

            scope.isActive = function (name) {
                return name === scope.activeTab ? 'is-active' : '';
            }
        }
    }
}

I want to mock the dependencies to unit test the different code paths with values known by the unit tests. I have the following sample unit test:

it('should set $scope.user to values passed by $userDetails', 
    inject(function($compile) {
        var element = '<div top-level-menu></div>';
        element = $compile(element)($scope);
        $scope.$apply();

        expect(element.isolateScope().user.name).toBe('test name');
        expect(element.isolateScope().user.avatar).toBe('test avatar');
    }
));

This gives me two problems.

First, since the template is in an external file, when it loads it tries to fetch it and errors out beacause the file is nowhere to be found, which is logical since it's in a test environment and not an actual server.

Second, there's no apparent way to mock the dependencies injected into the directive through its constructor. When testing controllers you can use the $controller service, but since directives are instantiated indirectly by compiling an html tag with a passed scope, there's no way to instantiate it directly (e.g. there's no analogous $directive). This impedes me from setting $userDetails.name and $userDetails.gravatar to 'test name' and 'test avatar' respectively.

How do I get the directive to compile properly and run with a custom $userDetails dependency?

like image 939
Nicolás Straub Avatar asked Dec 20 '13 01:12

Nicolás Straub


1 Answers

To load the template file you must configure karma-ng-html2js-preprocessor in karma.

First, visit this page and follow the installation instructions. Then, you need to add a couple of entries in your karma.config.js file:

files: [
    'templates/*.html'
],

this tells karma to load all html files in the templates folder (if your templates are somewhere else, put that folder there).

preprocessors: { '**/*.html': 'ng-html2js' },

this tells karma to pass all html files through the ng-html2js preprocessor, which then transforms them into angular modules that put the templates into the $templateCache service. This way, when $httpBackend queries the "server" for the template, it get's intercepted by the template cache and the correct html is returned. All fine here, except for the template's URL: it has to match the templateUrl property in the directive, and ng-html2js passes the full path as the uri by default. So we need to transform this value:

ngHtml2JsPreprocessor: {
    cacheIdFromPath: function(filepath) {

        var matches = /^\/(.+\/)*(.+)\.(.+)$/.exec(filepath);

        return 'templates/' + matches[2] + '.' + matches[3];
    }
},

this receives filepath and passes it through a regular expression that extracts the path, file name and extension into an array. You then prepend 'templates/ to the file name and extension and you get the expected uri.

After all this is done making the template available is a matter of loading the module before your test is run:

beforeEach(module('templates/TopLevelMenu.Template.html'));

keep in mind, module is an external service located in angular-mocks.js.

for injecting a custom service into the directive you need to override the service's provider:

beforeEach(module(function ($provide) {
    $provide.provider('$userDetails', function () { 
        this.$get = function () {
            return {
                name: 'test name', 
                gravatar: 'test avatar'
            };
        }
    });
}));

$provide is the service that provides your providers. So, if you want to inject a mock dependency you override the provider here.

With that code executing before your test you'll have a mock $userDetails service that returns your predefined strings.

like image 132
Nicolás Straub Avatar answered Sep 21 '22 14:09

Nicolás Straub