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);
});
Related
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
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'm going through the process of refactoring my controller function into more streamlined ones in my directives.
Am reasonably new to Angular and am running into problems mocking and testing my promises within the directives.
Within the function, I call a Box.reboot() from the directive rebootBox.
app.directive("rebootBox", ["Box", function(Box) {
return {
restrict: "A",
link: function( scope, element, attrs ) {
element.bind( "click", function() {
Box.reboot({id: scope.box.slug}).$promise.then(function(results) {
scope.box.state = 'rebooting';
}, function(errors) {
scope.box.errors = true;
})
});
}
}
}])
My tests pass in the controller specs because I am able to do something like this:
fakeFactory = {
reboot: function () {
deferred = q.defer();
return {$promise: deferred.promise};
}
...
}
MainCtrl = $controller('MainCtrl', {
$scope: scope,
Box: fakeFactory,
});
However, I can't get my head around how I am supposed to do this in my directive test?
I've tried this but I don't understand how I can mock what I did in the controller, ie:
Box: fakeFactory
My directive test looks like this so far:
describe('box reboot', function () {
var $scope,
element,
deferred,
q,
boxFactory;
beforeEach(module('myApp'));
beforeEach(inject(function($compile, $rootScope, $q) {
$scope = $rootScope;
q = $q;
element = angular.element("<div reboot-box></div>");
$compile(element)($rootScope)
boxFactory = {
reboot: function () {
deferred = q.defer();
return {$promise: deferred.promise};
}
};
}))
it("should reboot a box", function() {
spyOn(boxFactory, 'reboot').andCallThrough()
$scope.box = {}
element.click();
deferred.resolve({slug: 123});
$scope.$apply()
expect(boxFactory.reboot).toHaveBeenCalled();
});
...
Obvs. it fails because I'm spying on boxFactory.
What is the best way to go about testing such a function?
--- EDIT ----
Further to the comment below, I've used $provide to mock the service call:
beforeEach(module('myApp', function($provide) {
boxFactory = {
get: function () {
deferred = q.defer();
return {$promise: deferred.promise};
},
reboot: function () {
deferred = q.defer();
return {$promise: deferred.promise};
},
};
$provide.value("Box", boxFactory);
I can now call deferred.resolve successfully and all my tests pass bar one.
expect(boxFactory.reboot).toHaveBeenCalled();
Is there a specific reason why this fails and how can I get it to pass?
How do I check if $emit has been called in an unit test of a directive?
My directive is fairly simple (for the purpose of illustration):
angular.module('plunker', [])
.directive('uiList', [function () {
return {
scope: {
lengthModel: '=uiList',
},
link: function (scope, elm, attrs) {
scope.$watch('lengthModel', function (newVal) {
scope.$emit('yolo');
});
}
}
}])
so every time the attribute uiList changes, it will emit the event.
And I have unit test code as follows:
describe('Testing $emit in directive', function() {
var scope;
var element;
//you need to indicate your module in a test
beforeEach(function () {
module('plunker');
inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.row= 1;
spyOn(scope,'$emit');
element = angular.element('<ul id="rows" ui-list="row">');
$compile(element)(scope);
});
});
it('should emit', function() {
scope.$digest();
scope.row = 2;
scope.$digest();
expect(scope.$emit).toHaveBeenCalledWith("yolo");
});
});
This would always give me error stating that the scope.$emit has never been called.
Is there something wrong with the scope? Can someone please help?
Plunker:http://plnkr.co/edit/AqhPwp?p=preview
Your directive creates an isolated scope, which is calling $emit,so you need to spy on this one ;)
describe('Testing $emit in directive', function() {
var scope;
var element;
var elementScope;
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
scope.row = 1;
element = angular.element('<ul id="rows" ui-list="row">');
$compile(element)(scope);
scope.$digest();
elementScope = element.scope();
}));
it('should emit', function() {
// arrange
spyOn(elementScope, '$emit');
// act
scope.$apply(function() {
scope.row = 2;
});
// assert
expect(elementScope.$emit).toHaveBeenCalledWith("yolo");
});
});
Fixed plunker here :)
I'm not sure how did it work for glepetre (not sure that it does actually). For me it didn't. The thing is that the directive runs $new on incomming scope so the scope it creates inside and element.scope() are some thing different. You can check their $id fields. So it does not help to spy on the scope you got from element.scope(). I had smilar problem and had to do a little trick to make it work.
Here comes my test :
beforeEach(inject(function () {
scope = $rootScope.$new();
scope.$apply(function () {
scope.field = null;
scope.name = 'nolength';
});
spyOn(scope, '$new').and.returnValue(scope);
spyOn(scope, '$emit');
var element = angular.element('<my-directive name="name" field="field" title="\'Title\'" ></my-directive>');
noLengthValidation = $compile(element)(scope);
scope.$apply();
elementScope = element.scope();
}));
So the trick was to mock $new function on scope so instead of creating a new scope behind the scene and mess the things up it will return itself! Here comes the test itself :
it('The directive should not react on string length if no max-chars attribute presented', function () {
scope.$apply();
noLengthValidation.isolateScope().field = 'fieldisssssssveeeeerryyyyyylooooooonnnngggggggggggggggggggggggggggg';
scope.$apply();
expect(scope.$emit).toHaveBeenCalledWith('ON_MORE_AWASOME_EVENT', 'nolength');
});
And the the part on the directive that calls $emit. Just in case :
function validate() {
scope.validate = 1;
if (scope.field) {
if (!isValid()) {
scope.$emit('ON_AWASOME_EVENT', {code : scope.name, text : 'Field ' + scope.title + ' is feels bad'});
} else {
scope.$emit('ON_MORE_AWASOME_EVENT', scope.name);
}
}
}
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/