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
Related
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);
});
I am testing the directive below that uses an isolated scope. I know the triggerHandler is working but for some reason I keep getting the error
Expected undefined to equal 'http://www.gravatar.com/avatar/12345?s=40&d=identicon'.
Directive:
angular.module('pb.webSites.directives')
.directive('pbOrganizationImagePicker', [ function () {
return {
restrict: "E",
template: '<img data-ng-src="{{ imageSource }}" width="{{width}}" height="{{height}}" alt="Image Picker" class="img-rounded" />',
scope: {
fileId: '=pbFileId',
defaultSrc: '#pbDefaultSrc',
width: '#pbWidth',
height: '#pbHeight'
},
controller: 'pbOrganizationImagePickerController',
link: function (scope, element, attrs) {
scope.$watch('defaultSrc', function (value) {
if (value !== undefined) {
scope.imageSource = value;
}
});
element.on('click', function () {
scope.pickImage().then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
}
};
}]);
Tests:
describe('pbOrganizationImagePicker', function () {
beforeEach(module('pb.webSites.controllers'));
beforeEach(module('pb.webSites.directives'));
beforeEach(module('ui.router'));
beforeEach(module('ui.bootstrap'));
var compile;
var scope;
var mockModal = {};
var image;
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile
scope = $rootScope.$new();
}));
beforeEach(inject(function ($q, $injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET('/app/webSites/directives/OrganizationImagePicker.html').respond(200, '');
scopeObject = {
profileImageUrl: 'http://www.gravatar.com/avatar/12345?s=40&d=identicon',
profileImageId: 54634
};
scope.webSite = {
profileImageId: 6436
};
scope.pickImage = function () {
var defer = $q.defer();
defer.resolve(scopeObject);
return defer.promise;
};
}));
describe('element.click()', function () {
beforeEach(function () {
var html = angular.element('<pb-organization-image-picker data-pb-default-src="{{ webSite.profileImageUrl || \'/content/img/placeholder-lg.jpg\' }}" data-pb-file-id="webSite.profileImageId" data-pb-width="200"></pb-organization-image-picker>');
element = compile(html)(scope);
element.triggerHandler('click');
});
it('should assign value to scope variables', function () {
scope.pickImage();
scope.$digest();
expect(scope.imageSource).toEqual(scopeObject.profileImageUrl);
expect(scope.fileId).toEqual(scopeObject.profileImageId);
});
});
});
I have also tried changing the test to the following since Im pretty sure in the test above I am faking the test a bit. However here I get pickImage() was never called. Even if you dont see the problem which method do you think is better for testing?
describe('element.click()', function () {
it('should assign value to scope variables', function () {
element = compile(html)(scope);
spyOn(scope, 'pickImage');
element.triggerHandler('click');
scope.$apply();
//scope.pickImage();
expect(scope.pickImage).toHaveBeenCalled();
scope.$digest();
expect(scope.imageSource).toEqual(scopeObject.profileImageUrl);
expect(scope.fileId).toEqual(scopeObject.profileImageId);
});
});
element.on('click', function () {
scope.$apply(function() {
scope.pickImage().then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
});
Wrap the code in your click handler in an $apply.
I suspect the real problem is that your directive uses an isolate scope, so it actually won't have a "pickImage()" method and when you assign imageSource and fileId, you are putting them on the directive scope, and not the scope that your test is trying to validate (the parent scope).
Your test assigns pickImage() and webSite to the scope of your test element. Since you use an isolated scope, your directive won't have access to these methods and properties. You should probably move these to a service and inject them into the directive.
It's not "correct", but to test the theory you can change your directive to:
element.on('click', function () {
scope.$apply(function(){
scope.$parent.pickImage().then(function (image) {
scope.$parent.imageSource = image.storageUrl;
scope.$parent.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
});
This IS NOT something you want in production code, but I'm just trying to demonstrate how the different scopes are related to each other.
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));
}));
});
Reference: Unit Testing AngularJS Directives: scopes not updating?
Case
I have a directive called editable that take an ng-model and creates a toggleable/editable field. The directive works and the parent scope is updated correctly, there are no problems in the actual function of the directive. I cannot seem to write a test that supports this though. It took me a long time to get the directive working properly with all the caveats so I really want to get some tests in place to make sure it continues to work in all of the different cases.
The Directive (which works)
I can't be sure which pieces will be relevant so i included the whole thing.
app.directive('editable',
['$templateCache', '$compile',
function ($templateCache, $compile) {
return {
restrict: 'A',
transclude: true,
templateUrl: 'template/directives/editable.html',
replace: true,
require: 'ngModel',
scope: true,
compile: function(element, attrs, transcludeFn) {
return function (scope, iElement, iAttrs, ctrl) {
var validityId = 'editable-' + scope.$id;
scope.lastSavedValue = iElement.find('input').val();
scope.storeValue = function() {
scope.lastSavedValue = iElement.find('input').val();
};
scope.edit = function() {
scope.storeValue();
scope.editing = true;
$('input', iElement).focus().select();
ctrl.$setValidity(validityId, true);
};
scope.ok = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(inputCtrl.$valid === true) {
scope.editing = false;
scope.value = inputCtrl.$viewValue;
ctrl.$setValidity(validityId, false);
ctrl.$setViewValue(inputCtrl.$viewValue); // Not sure (why) this is needed
}
};
scope['delete'] = function() {
scope.deleted = true;
scope.editing = false;
};
scope.undo = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(scope.lastSavedValue) {
inputCtrl.$setViewValue(scope.lastSavedValue);
scope.value = scope.lastSavedValue;
ctrl.$setViewValue(scope.lastSavedValue);
}
iElement.find('input').val(scope.value);
scope.editing = false;
scope.deleted = false;
ctrl.$setValidity(validityId, false);
};
transcludeFn(scope, function(clone) {
var $editingReplacement = $(clone).filter('[editing]');
var $viewingReplacement = $(clone).filter('[viewing]');
var $deletedReplacement = $(clone).filter('[deleted]');
var $viewingControls = $('.editable-view-container .controls', iElement);
var $editingControls = $('.editable-input-container .controls', iElement);
var $deletedControls = $('.editable-delete-container .controls', iElement);
if($editingReplacement.length) {
$('.editable-input-container', iElement).html($editingReplacement.html());
$('.editable-input-container', iElement).append($editingControls);
$compile($('.editable-input-container', iElement))(scope);
} else {
$('.editable-input-container', iElement).find('input').attr('ng-model', iAttrs['ngModel']);
$compile($('.editable-input-container', iElement))(scope);
}
if($viewingReplacement.length) {
$('.editable-view-container', iElement).html($viewingReplacement.html());
$('.editable-view-container', iElement).append($viewingControls);
$compile($('.editable-view-container', iElement))(scope);
}
if($deletedReplacement.length) {
$('.editable-delete-container', iElement).html($deletedReplacement.html());
$('.editable-delete-container', iElement).append($deletedControls);
}
});
/**
* Deleted (Isolated Scope)
*
* Tracks if the user has clicked the delete button
*
* #type {Boolean}
*/
scope.deleted = false;
/**
* Editing (Isolated Scope)
*
* Tracks the state of the view
*
* #type {Boolean}
*/
scope.editing = false;
/**
* Initial Loader
*
* Run once after ctrl is loaded
*
* #return {[type]} [description]
*/
var unbindWatcher = scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
if(typeof ctrl.$modelValue !== 'undefined') {
scope.value = ctrl.$modelValue;
scope.editing = ctrl.$modelValue ? false : true;
unbindWatcher();
}
});
};
}
};
}
]);
Spec
Fails at the end
describe('Editable Directive', function() {
// Keep references to element and scope so that they are available to all tests
var element, scope, ctrl;
beforeEach(module('ltAccountApp'));
beforeEach(module('templates'));
beforeEach(inject(function ($rootScope, $compile) {
var linkFn, el;
// scope = $rootScope;
scope = $rootScope.$new();
scope.testValue = 'xxx';
el = angular.element('\
<div editable="" ng-model="testValue"></div>\
');
// The $compile method returns the directive's link function
linkFn = $compile(el);
// The link function returns the resulting DOM object
element = linkFn(scope);
element.scope().$apply();
ctrl = element.controller('ngModel');
}));
it('should assign input value to scope value', function() {
expect(element.find('input').val()).toEqual(scope.testValue);
});
it('should have access to parent scope variable passed into directive', function() {
expect(ctrl.$viewValue).toEqual(scope.testValue);
expect(ctrl.$modelValue).toEqual(scope.testValue);
});
it('should manage state editing correctly', function() {
expect(element.scope().editing).toBe(false);
element.scope().edit();
expect(element.scope().editing).toBe(true);
});
it('should manage state deleted correctly', function() {
expect(element.scope().deleted).toBe(false);
element.scope()['delete']();
expect(element.scope().deleted).toBe(true);
});
it('should be able to modify parent scope variable passed into directive', function() {
// Not sure what this does, added from referenced SO question
// spyOn(scope, '$apply').andCallThrough();
var newValue = 'yyy';
element.scope().edit();
element.find('input').val(newValue);
element.find('input').trigger('input');
element.scope().ok();
expect(ctrl.$viewValue).toEqual(newValue);
expect(ctrl.$modelValue).toEqual(newValue);
expect(element.scope().value).toEqual(newValue);
expect(scope.$apply).toHaveBeenCalled();
expect(scope.testValue).toEqual(newValue); // <-fails
});
});
So...
Everything seems to be working until I actually expect the parent scope to have the changed value.
I know there is a lot here, I appreciate any guidance you can provide.
Plunk of Angular + Jasmine with directive (work in progress)
Lets say you have to update the value of input field to 'kasrak'. Trying doing it like this:
var elm = element.find("input");
elm.val('kasrak');
elm.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest()
Key is the third line with $sniffer service. I found this in angular-ui's bootstrap tests.
Here is the link to the function in test spec: https://github.com/angular-ui/bootstrap/blob/master/src/typeahead/test/typeahead.spec.js#L31
describe("create a simple directive", function () {
var simpleModule = angular.module("directivesSample", []);
simpleModule.controller('Ctrl2', function ($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
});
simpleModule.directive("myCurrentTime", function ($timeout, dateFilter) {
return function (scope, element, attr) {
var format;
var timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attr.myCurrentTime, function (value) {
format = value;
updateTime();
});
function updateLater() {
timeoutId = $timeout(function () {
updateTime();
updateLater();
}, 1000);
}
element.bind('$destroy', function () {
$timeout.cancel(timeoutId);
});
updateLater();
}
});
beforeEach(module('directivesSample'));
var element = angular.element(
'<div ng-controller="Ctrl2">Date format:<input ng-model="format"> ' +
'<hr/>Current time is: ' +
'<span class="timeout" my-current-time="format" id="timeout-render"></span>' +
'</div>');
var directiveScope;
var scope;
var linkedElement;
var linkFunction;
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
linkFunction = $compile(element);
linkedElement = linkFunction(scope);
scope.$apply();
}));
it("should define element time out", function () {
var angularElement = element.find('span'); // <-- element is not returned if set to var angularElement = element.find('.timeout'); or var angularElement = element.find('#timeout-render');
console.log(angularElement.text());
expect(angularElement.text()).not.toBe('');
})
});
Having the above test, why am I unable to search for element by JQuery selector ? I am aware of the limitations in the documentation of find() method.But, I have checked out the angularUI project inspected the the usage of find() function as in here
var tt = angular.element(elm.find("li > span")[0]);
and find out that the guys are using find to search element by jQuery elector not only tag name while I am not able to do so. Am I missing something ?
That's because the built in jqLite in Angular has only limited support for CSS selectors. But if you include jQuery in a script tag before you include Angular, Angular will see that and use jQuery instead of its jqLite for calls to angular.element().