In our app we have several layers of nested directives. I\'m trying to write some unit tests for the top level directives. I\'ve mocked in stuff that the directive itself ne
Loved Sylvain's answer so much I had to turn it into a helper function. Most often, what I need is to kill off a child directive so that I can compile and test the parent container directive without its dependencies. So, this helper lets us do that:
function killDirective(directiveName) {
angular.mock.module(function($compileProvider) {
$compileProvider.directive(directiveName, function() {
return {
priority: 9999999,
terminal: true
}
});
});
}
With that, you can completely disable a directive by running this before the injector gets created:
killDirective('myLowerLevelDirective');
Being forced to think about this more myself, I have come up with a solution that fills our needs. All of our directives are attributes, so I created an attributeRemover
directive for use during the unit tests. It looks something like this:
angular.module("myModule").directive("attributeRemover", function() {
return {
priority: -1, //make sure this runs last
compile: function(element, attrs) {
var attributesToRemove = attrs.attributeRemover.split(",");
angular.forEach(attributesToRemove, function(currAttributeToRemove) {
element.find("div[" + currAttributeToRemove + "]").removeAttr(currAttributeToRemove);
});
}
}
});
Then the html for the directive I'm testing looks something like this:
<div my-higher-level-directive attribute-remover="my-lower-level-directive,another-loweler-level-directive"></div>
So, when my-higher-level-directive
gets compiled the attribute-remover
will have already removed the attributes for the lower level directives and thus I don't have to worry about what they are doing.
There's probably a more robust way of doing this for all kinds of directives (not just attribute ones) and I'm not sure if this works if only using the built-in JQLite, but it works for what we need.
Due to the implementation of the directive registration, it does not seem possible to replace an existing directive by a mocked one.
However, you have several ways to unit test your higher level directive without interference from lower level directives :
1) Do not use lower level directive in your unit test template :
If your lower level directive is not added by your higher level directive, in your unit test use a template with only you higer-level-directive :
var html = "<div my-higher-level-directive></div>";
$compile(html)(scope);
So, lower level directive will not interfere.
2) Use a service in your directive implementation :
You can provide the lower level directive linking function by a service :
angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
link: myService.lowerLevelDirectiveLinkingFunction
}
});
Then, you can mock this service in your unit test to avoid interference with your higher level directive. This service can even provide the whole directive object if needed.
3) You can overwrite your lower level directive with a terminal directive :
angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
priority: 100000,
terminal: true,
link: function() {
// do nothing
}
}
});
With the terminal option and a higher priority, your real lower level directive will not be executed. More infos in the directive doc.
See how it works in this Plunker.
Here is another small idea. Just put this code in jasmine helpers (coffee script)
window.mockDirective = (name, factoryFunction) ->
mockModule = angular.module('mocks.directives', ['ng'])
mockModule.directive(name, factoryFunction)
module ($provide) ->
factoryObject = angular.injector([mockModule.name]).get("#{name}Directive")
$provide.factory "#{name}Directive", -> factoryObject
null
And use it:
beforeEach mockDirective, "myLowerLevelDirective", ->
link: (scope, element) ->
This will completely remove all other implementations of given directive, giving a full access to test passed arguments to the directive. FOr example, mm.foundation alert directive can be mocked with:
beforeEach mockDirective 'alert', ->
scope:
type: '='
and then tested:
expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual
You can modify your templates inside $templateCache
to remove any lower level directives:
beforeEach(angular.mock.inject(function ($templateCache) {
$templateCache.put('path/to/template.html', '<div></div>');
}));
The clean way of mocking a directive is with $compileProvider
beforeEach(module('plunker', function($compileProvider){
$compileProvider.directive('d1', function(){
var def = {
priority: 100,
terminal: true,
restrict:'EAC',
template:'<div class="mock">this is a mock</div>',
};
return def;
});
}));
You have to make sure the mock gets a higher priority then the directive you are mocking and that the mock is terminal so that the original directive will not be compiled.
priority: 100,
terminal: true,
The result would look like the following:
Given this directive:
var app = angular.module('plunker', []);
app.directive('d1', function(){
var def = {
restrict: 'E',
template:'<div class="d1"> d1 </div>'
}
return def;
});
You can mock it like this:
describe('testing with a mock', function() {
var $scope = null;
var el = null;
beforeEach(module('plunker', function($compileProvider){
$compileProvider.directive('d1', function(){
var def = {
priority: 9999,
terminal: true,
restrict:'EAC',
template:'<div class="mock">this is a mock</div>',
};
return def;
});
}));
beforeEach(inject(function($rootScope, $compile) {
$scope = $rootScope.$new();
el = $compile('<div><d1></div>')($scope);
}));
it('should contain mocked element', function() {
expect(el.find('.mock').length).toBe(1);
});
});
A few more things:
When you create your mock, you have to consider whether or not you need replace:true
and/or a template
. For instance if you mock ng-src
to prevent calls to the backend, then you don't want replace:true
and you don't want to specify a template
. But if you mock something visual, you might want to.
If you set priority above 100, your mocks's attributes will not be interpolated. See $compile source code. For instance if you mock ng-src
and set priority:101
, then you'll end-up with ng-src="{{variable}}"
not ng-src="interpolated-value"
on your mock.
Here is a plunker with everything. Thanks to @trodrigues for pointing me in the right direction.
Here is some doc that explains more, check the "Configuration Blocks" section. Thanks to @ebelanger!