I know there is $controllerProvider which can be used, to create controllers inside unit tests. I'm wondering however if there is a way to mock a directive inside of a unit test.
The background to this is, that I'm trying to mock(! not to create) a directive which is applying an async validator to ngModel. Perfect would be something like this:
beforeEach(function () {
module('myMod', function($directiveProvider) {
$directiveProvider.register('mockAsyncValidationDirective', function() {
return {
restrict: 'EA',
require: 'ngModel'
// ...
}
});
});
});
Is there any way to do that?
I haven't found a way to mock a directive (maybe there isn't one atm), but I was able to solve my specific issue, by accessing the model-controller via the form controller.
Here is the code:
var scope = $rootScope.$new();
var html = '<form name="testForm">' +
'<input type="text" ng-model="username">' +
'</form>';
var element = $compile(html)(scope);
var form = scope.testForm;
var ngModel = form.username;
ngModel.$asyncValidators.unique = function (modelValue, viewValue)
{
var deferred = $q.defer();
$timeout(function ()
{
if (viewValue === 'NOT_UNIQUE') {
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
};
I still would be interested in an answer on how to mock a full directive.
Related
I have the following directive. This Directive will trigger after animation end triggers. I now just want to add test for this directive. I have created the test case for this. But it's failing. Can anybody help on this? This is plunker
angular.module('movieApp')
.directive('animationend', function() {
return {
restrict: 'A',
scope: {
animationend: '&'
},
link: function(scope, element) {
var callback = scope.animationend(),
events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
element.on(events, function(event) {
console.log('[animationend directive] callback');
callback.call(element[0], event);
});
}
};
});
And my test case is
describe('Directive: animationend', function () {
// load the directive's module
beforeEach(module('movieApp'));
var elements,
scope,
compile;
beforeEach(inject(function ($rootScope, _$compile_) {
scope = $rootScope.$new();
compile = _$compile_;
elements = angular.element('<div animationend="" ></div>');
elements = compile(elements)(scope);
scope.$digest();
}));
it('should trigger the callback once animationclass is added ', function () {
scope.ctrl = {
animationend: jasmine.createSpy('animationend')
};
scope.$digest();
var events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
angular.element(elements).triggerHandler(events);
// element.trigger(events);
expect(scope.ctrl.animationend).toHaveBeenCalled();
});
});
I have the following directive which tells me whether or not the image i'm trying to use has loaded successfully or not:
return {
restrict: 'A',
scope: {
imageLoad: '#'
},
link: function(scope, element, attrs) {
attrs.$observe('imageLoad', function (url) {
var deferred = $q.defer(),
image = new Image();
image.onerror = function () {
deferred.resolve(false);
};
image.onload = function () {
deferred.resolve(true);
};
image.src = url;
return deferred.promise;
});
}
};
All i then want to do is two simple tests that test image.onerror and image.onload but i only seem to get into the on error function, here's what i have so far:
compileDirective = function() {
var element = angular.element('<div data-image-load="http://placehold.it/350x150"></div>');
$compile(element)(scope);
$rootScope.$digest();
return element;
};
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
}));
it('should do something', function() {
var compiledElement, isolatedScope;
compiledElement = compileDirective();
isolatedScope = compiledElement.isolateScope();
expect(true).toBe(true);
});
obviously this test passes as it just expects true to be true, however in terms of coverage this gets into the onerror function, so i somehow need to test that the deferred.promise should return false.
so ultimately a two part question, how do i get the result of the deferred.resolve?
and secondly how do i get into the onload function?
i've had a look around and seen some suggestions of adding the following:
element[0].setAttribute('imageLoad','http://placehold.it/350x150');
$compile(element)(scope);
element.trigger('imageLoad');
and leaving the data-image-load="" blank, but haven't seemed to have any luck, any suggestions would be great.
From what you have shown, you shouldn't need the promise at all.
Even if you did, since the promise is only used internally and no one is using the result, it should be considered an implementation detail and your test shouldn't care about it.
Let us say you have the following directive:
app.directive('imageLoad', function() {
return {
restrict: 'A',
scope: {
imageLoad: '#'
},
link: function(scope, element, attrs) {
var fallback = 'http://placekitten.com/g/200/300';
attrs.$observe('imageLoad', function(url) {
var image = new Image();
image.onerror = function(e) {
setBackground(fallback);
};
image.onload = function() {
setBackground(url);
};
image.src = url;
});
function setBackground(url) {
element.css({
'background': 'url(' + url + ') repeat 0 0'
});
}
}
};
});
Demo of it in use: http://plnkr.co/edit/3B8t0ivDbqOWU2YxgrlB?p=preview
From an outside perspective, the purpose of the directive is to set the element's background to either the passed url or to the fallback, based on if the passed url is working.
So what you want to test is:
The passed url is working - should use passed url as background.
The passed url is not working - should use fallback as background.
This means that you need to be able to control if the image can be loaded or not.
To prevent any network traffic in your test I would recommend using data URIs instead of URLs.
Example:
var validImage = 'data:image/jpeg;base64, + (Valid data omitted)';
var invalidImage = 'data:image/jpeg;base64,';
Full example:
describe('myApp', function() {
var $compile,
$rootScope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
var validImage = 'data:image/jpeg;base64, + (Valid data omitted)';
var invalidImage = 'data:image/jpeg;base64,';
compileDirective = function(url) {
var element = angular.element('<div data-image-load="' + url + '"></div>');
return $compile(element)($rootScope.$new());
};
it('should use correct background when image exists', function(done) {
var element = compileDirective(validImage);
$rootScope.$digest();
setTimeout(function() {
var background = element.css('background');
expect(background).toBe('url("' + validImage + '") 0px 0px repeat');
done();
}, 100);
});
it('should use fallback background when image does not exist', function(done) {
var element = compileDirective(invalidImage);
$rootScope.$digest();
setTimeout(function() {
var background = element.css('background');
expect(background).toBe('url("http://placekitten.com/g/200/300") 0px 0px repeat');
done();
}, 100);
});
});
Note that since loading of an image is asynchronous you need to add a bit of waiting in your tests.
You can read more about how it is done with Jasmine here.
Demo: http://plnkr.co/edit/W2bvHih2PbkHFhhDNjrG?p=preview
So here is my angular directive. Simple one that uses a template url
angular.module('my.directives')
.directive('userNameDisplay', function() {
return {
restrict: 'E',
scope: {
user: '=user',
status: '=status'
},
templateUrl: '/partials/userNameDisplay.html'
};
});
The spec is as follows. Again it tries to cover all cases.
describe('user-name-display', function () {
var elm, scope;
beforeEach(module('my.directives', '/partials/userNameDisplay.html'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
elm = angular.element('<user-name-display user="someUser" status="status"></user-name-display>');
$compile(elm)(scope);
}));
it('should have the correct isolate scope values', function () {
scope.someUser = {
name: "John",
color: "blue"
};
scope.status = true;
scope.$digest();
var isoScope = elm.isolateScope();
expect(isoScope.user.name).toBe('John');
expect(isoScope.displayCollaboratorStatus).toBe(true);
});
it('should render html within the partial accordingly', function () {
scope.someUser = {
name: "John"
};
scope.status = false;
scope.$digest();
var cBoxSpan = elm.find("span.user-collaborator-box");
expect(cBoxSpan.length).toBe(0);
var userNameBox = elm.find("span.user-name");
expect(userNameBox[0].innerHTML).toBe("John");
});
});
The coverage report looks like the one below. I am using Karma (which uses Istanbul) to get the code coverage. I am trying to increase it to 100%. I can't figure out from the report what I am missing. It says return statement was never hit, but without it, the isolate bindings will not take place. How can I get the coverage to go 100%?
Here is the image of the report
http://imgur.com/NRzTjyZ
I don't think you'll get coverage from a beforeEach block.
Try adding this test (it's identical to your beforeEach code):
it('should compile', function() {
scope = $rootScope;
elm = angular.element('<user-name-display user="someUser" status="status"></user-name-display>');
$compile(elm)(scope);
});
SOLUTION = Solution Plunker
I have tried manually passing the template in testing - is it even good way of doing it ? How to make it passes !!!!!!!
How to write unit test for Simple directive with template-Url without Using Karma . I have tried following after seeing examples on stack-overflow but no success.
Directive
app.directive("crazy", function () {
return {
restrict: "A",
templateUrl:"directivetemplate.html"
};
});
Spec
describe('Directive: crazy', function () {
beforeEach(module('plunker'));
beforeEach(inject(function($templateCache) {
var directiveTemplate = null;
var req = new XMLHttpRequest();
req.onload = function() {
directiveTemplate = this.responseText;
};
req.open("get", "directivetemplate.html", false);
req.send();
$templateCache.put("directiveTemplate.html", directiveTemplate);
}));
it('should exist', inject(function ($rootScope, $compile) {
element = angular.element('<div crazy></div>');
element = $compile(element)($rootScope);
$rootScope.$apply();
expect(element.children().length.toBe(2));
}));
});
I have the following directive.
directivesModule.directive('wikis', function() {
var urlRegex = new RegExp('^(https?)://.+$');
return {
restrict: 'E',
templateUrl: 'templates/wiki-list.html',
scope: {
wikis: '='
},
link: function(scope, element, attrs) {
scope.newWikiURL = '';
scope.$watch('wikis', function() {
if (scope.wikis == undefined || scope.wikis.length === 0) {
scope.class = 'hide';
} else {
scope.class = 'show';
}
}, false);
scope.addWiki = function() {
if (scope.wikis == undefined) {
scope.wikis = [];
}
var nw = scope.newWikiURL;
if (nw != undefined && nw.trim() != '' && urlRegex.exec(nw)) {
scope.wikis.push(nw);
scope.newWikiURL = '';
}
}
}
};
});
When I test it:
describe('Wikis Directive Test Suite', function() {
var scope, elem, directive, linkFn, html;
beforeEach(function() {
html = '<wikis wikis=''></wikis>';
inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.wikis = [];
elem = angular.element(html);
$compile(elem)(scope);
scope.$digest();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
I get an error saying that Object doesn't have an addWiki method. I tried to debug it, and the link function is not called during the test. I suspect that's why the addWiki method is not part of the scope. Anybody knows why I'm getting this error?
By the way, Is it a normal practice to add some logic into the link function of a directive as it would be a Controller itself? Because looking at the code I know that it's why in reality I'm doing.
As per angular 1.2.0 docs, the way to get the isolate scope is through the method isolateScope
scope() - retrieves the scope of the current element or its parent.
isolateScope() - retrieves an isolate scope if one is attached directly to the current element. This getter should be used only on elements that contain a directive which starts a new isolate scope. Calling scope() on this element always returns the original non-isolate scope.
Angular doc - section jQuery/jqLite Extras
BREAKING CHANGE: jqLite#scope()
You need to load the module containing your directive, otherwise angular doesn't know what <wikis> is
Your directive creates an isolate scope, so once it has been compiled you need to get the new scope using elem.isolateScope()
So with those changes:
describe('Wikis Directive Test Suite', function() {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function() {
html = '<wikis></wikis>';
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
$compile(elem)($scope);
scope = elem.isolateScope();
scope.$apply();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
http://jsfiddle.net/QGmCF/1/