I wrote a directive that will conditionally add a wrapper element which I modeled after Angular's ngIf directive. The directive works great when running in production, but in trying to add unit tests the $animate.enter function never calls my callback function. This is causing all my unit tests to fail when it assumes that the wrapper is not suppose to be there.
I'm using Angular.js version 1.2.16 and loading ngMock and ngAnimate for the unit test. The code fires the ngAnimate enter function, but then it never fires the callback.
You can view the code here, just uncomment the appSpec.js script tag and the directive no longer works.
Does anyone now how to trigger $animate.enter to call my callback function in a unit test?
addWrapperIf.js
angular.module('myModule', ['ngAnimate'])
.directive('addWrapperIf', ['$animate', function($animate) {
return {
transclude: 'element',
priority: 1000,
restrict: 'A',
compile: function (element, attr, transclude) {
return function ($scope, $element, $attr) {
var childElement, childScope;
$scope.$watch($attr.addWrapperIf, function addWrapperIfWatchAction(value) {
if (childElement) {
$animate.leave(childElement);
childElement = undefined;
}
if (childScope) {
childScope.$destroy();
childScope = undefined;
}
// add the wrapper
if (value) {
childScope = $scope.$new();
transclude(childScope, function (clone) {
childElement = clone
$animate.enter(clone, $element.parent(), $element);
});
}
// remove the wrapper
else {
childScope = $scope.$new();
transclude(childScope, function (clone) {
$animate.enter(clone, $element.parent(), $element, function() {
childElement = clone.contents();
clone.replaceWith(clone.contents());
});
});
}
});
}
}
};
}]);
addWrapperIfSpec.js
var expect = chai.expect;
describe('addWrapperIf', function () {
var $template;
var $compile;
var $scope;
beforeEach(window.module('myModule'));
beforeEach(inject(function(_$compile_, $rootScope){
$compile = _$compile_;
$scope = $rootScope.$new();
}));
function compileDirective(template) {
$template = $compile(template)($scope)[0];
$scope.$apply();
}
it('should output the correct values with default options', function() {
compileDirective('<div add-wrapper-if="false"><span>child</span></div>');
console.log($template); // <div add-wrapper-if="false"><span>child</span></div>
});
});
So I figured out what you have to do. I dug into the code and found out that inside ngAnimate it pushes the callback function to $$asyncCallback. $$asyncCallback has a flush function that will call any functions pushed onto it. To get $animate.enter to fire the callback, you have to inject $$asyncCallback into your unit test and then call $$asyncCallback.flush(). This will then run your callback function.
You can see this in this Plunker.
Related
i have a function which i call more than once ( in different directives).
So is there a way to call a function in every directive?
One solution would be to make the function as a service.
But the service does need a return value, doesn´t it?
this.changeDesign = function (currentstep) {
//do something
};
this method I call many times.
You should consider to avoid using global variables or put something on the global scope. Maybe it works now, but if your project gets bigger, you will probably get a lot of problems. More infos for example: https://gist.github.com/hallettj/64478
Here is an example, how you can use a factory, which you can inject into your directives (coding style inspired by John Papa https://github.com/johnpapa/angular-styleguide#factories):
(function () {
"use strict";
angular.module('my-app').provider('MyFactory', MyFactory);
MyFactory.$inject = [];
function MyFactory() {
var service = {
changeDesign: myChangeDesignImpl
};
function myChangeDesignImpl() { }
this.$get = function() {
return service ;
};
}
})();
You can now inject your service into a directive like this:
(function () {
"use strict";
angular.module('my-app').directive('MyDirective', MyDirective);
MyDirective.$inject = ["MyFactory"];
function MyDirective(MyFactory) {
var directive = {
restrict: 'E',
template: "/template.html",
link: link
};
return directive;
function link(scope, el, attr) {
MyFactory.changeDesign();
}
}
})();
you can do it in a directive that you will delcare on your elements.
angular.module('myDesignModule', [])
.directive('myDesignDirective', function() {
return {
link: function(scope, element, attrs, ctrl){
element.addClass("myClass");
}
};)
I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.
I have an Angular app defined within a Grails gsp. The Modernizr library is included at the gsp level.
I need to use the library within a directive unit test. Since I don't have Modernizr defined as a module since it's used outside of the Angular app as well as inside it, how do I inject it into my Angular unit test?
Here's my directive:
'use strict';
angular.module('simplify.directives').directive('img', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function (elem, attrs) {
if ( typeof Modernizr !== 'undefined' && !Modernizr.svg ) {
$timeout(function(){
elem.attr('src', attrs.src.replace('.svg', '.png'));
});
}
}
};
}]);
Here's my unit test code:
'use strict';
describe('Testing SVG to PNG directive', function() {
var scope,
elem;
beforeEach(module('app'));
beforeEach(module(function($provide) {
$provide.service('appConstants', function(){});
}));
beforeEach(inject(function($compile, $rootScope) {
elem = angular.element('<img ng-src="test-img.svg" />');
scope = $rootScope;
$compile(elem)(scope);
scope.$digest();
}));
it('Should swap svg for png image if svg is not supported', function() {
//force Modernizr.svg to be undefined here for purposes of the test
expect(elem.attr('src')).toBe('test-img.png');
});
});
What's the best-practice way to do this?
You could do this by updating the directive to inject $window and get Modernizr instance from $window.
i.e:-
.directive('img', ['$timeout','$window', function ($timeout, $window) {
return {
restrict: 'E',
link: function (scope, elem, attrs) {
if ( typeof $window.Modernizr !== 'undefined' && !$window.Modernizr.svg ) {
$timeout(function(){
elem.attr('src', attrs.src.replace('.svg', '.png'));
});
}
}
};
}]);
In your test just create a mock Modernizr and since you are using $timeout you would need to flush the timeout by doing $timeout.flush() inorder for timeout callback to get executed.
describe('Testing SVG to PNG directive', function() {
var scope,
elem,
ModernizerMock = {svg:false};// <-- use this for mocking various conditions
beforeEach(module('app'));
beforeEach(module(function($provide) {
$provide.service('appConstants', function(){});
}));
beforeEach(inject(function($compile, $rootScope, $timeout, $window) {
elem = angular.element('<img ng-src="test-img.svg" />');
scope = $rootScope;
$window.Modernizr = ModernizerMock; //Set the mock
$compile(elem)(scope);
scope.$digest();
$timeout.flush(); //<-- Flush the timeout
}));
it('Should swap svg for png image if svg is not supported', function() {
//force Modernizr.svg to be undefined here for purposes of the test
expect(elem.attr('src')).toBe('test-img.png');
});
});
Plnkr
and had your directive been like this:
.directive('img', ['$window', function ($window) {
return {
restrict: 'E',
compile: function (elem, attrs) {
if ( typeof $window.Modernizr !== 'undefined' && !$window.Modernizr.svg ) {
attrs.$set('ngSrc', attrs.ngSrc.replace('.svg', '.png'));
}
}
};
}]);
It would be lesser effort to test with the absence of timeout logic. Plnkr
What I want to do:
I'm trying to create a function within a directive that can be called from the $rootScope.
The Problem:
It seems to only be working on the last item in the DOM which has this directive. I'm guessing that what's happening is rootScope.myFunction gets overwritten each time this directive runs.
The Question:
How can I create one function in the $rootScope which, when called, runs the internal function for each directive instead of just the last one?
The Relevant Code:
(function() {
angular.module('home')
.directive('closeBar', closeBar);
closeBar.$inject = ['$rootScope', '$window'];
function closeBar(rootScope, window) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
var myFunction = function() {
// do stuff
};
myFunction();
rootScope.myFunction = function() {
myFunction();
};
}
};
}
})();
Then in a different script, I want to call:
rootScope.myFunction();
Note, I'm not allowed to share actual code from the project I'm working on so this is a more general question not tied to a specific use case.
A simple solution would be to create a special function in your rootScope that accepts functions as an argument, and pushes it into an array, and you will be able to invoke that function later which will call all the registered functions.
angular.module('myApp').run(['$rootScope', function($root) {
var functionsToCall = [];
$root.registerDirectiveFunction = function(fn, context) {
context = context || window;
functionsToCall.push({fn: fn, context: context});
}
$root.myFunction = function(args) {
functionsToCall.forEach(function(fnObj) {
fnObj.fn.apply(fnObj.context,args);
});
}
}
And in your directive:
link: function($scope, $el, $attr) {
function myFunct() {
}
$scope.$root.registerDirectiveFunction(myFunct, $scope);
//call it if you want
$scope.$root.myFunction();
}
This should cover your scenario.
I am about to give up on this. I have tried every which way to access the directive scope in a test.
'use strict';
angular.module('cmsModuleApp')
.directive('fileUpload', function () {
return {
scope: {},
template: '<input type="file" >',
restrict: 'E',
controller: function fileUploadCtrl (scope) {
//also tried scope.uploadFile here...
this.uploadFile = function (files) {console.log("please work...")};
},
link: function postLink(scope, element, attrs, Ctrl) {
element.uploadFile = function (files) {console.log("pleaseeeee")};
}
};
});
test::
'use strict';
describe('Directive: fileUpload', function () {
beforeEach(module('cmsModuleApp'));
var element;
var scope;
var files;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
}));
it('should call a file upload method onchange event', inject(function ($compile) {
element = angular.element('<file-upload></file-upload>');
element = $compile(element)(scope);
//tried moving this around thinking maybe it had to render or update
scope.$digest();
//Im loggin the element's scope to explore the object a bit
console.log(element.scope());
spyOn(element.scope(), 'uploadFile')
element.triggerHandler('onchange');
expect(element.scope().uploadFile()).toHaveBeenCalled();
}));
});
What I am trying to test is that when this file input changes (is clicked and loaded up with files) it will execute the uploadFile() method on the directive's scope. Once I get this working I was going to implement an $http service.
However, the method does not exist or is undefined.. No matter what I seem to try.
Could you try to modify your test file like this?
I moved the variables declaration into the describe and the test initilization into the beforeEach. Then I created a spy on scope.uploadFile.
fileUpload_test :
'use strict';
describe('Directive: fileUpload', function () {
var element;
var scope;
var files;
beforeEach(module('cmsModuleApp'));
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
element = angular.element('<file-upload></file-upload>');
element = $compile(element)(scope);
scope.$digest();
}));
afterEach(function() {
scope.$destroy();
});
it('should call a file upload method onchange event', function() {
scope.uploadFile = jasmine.createSpy();
element.triggerHandler('change');
expect(scope.uploadFile).toHaveBeenCalled();
}));
});
I think the issue might be that you are using an isolate scope scope: {}. Here's an example of how I did a similar task:
describe('File Input Directive', function() {
var scope, element, isolateScope;
beforeEach(function() {
bard.appModule('appName');
bard.inject(this, '$compile', '$rootScope');
scope = $rootScope.$new();
var html = '<form><my-file-input /></form>';
var form = angular.element(html);
element = form.find('my-file-input');
var formElement = $compile(form)(scope);
scope.$digest();
isolateScope = element.isolateScope();
});
afterEach(function() {
scope.$destroy();
});
bard.verifyNoOutstandingHttpRequests();
describe('selectFile', function() {
it('triggers a click on the file input', function() {
var fileInput = $(element).find('.none')[0];
var mockClick = sinon.spy(fileInput, 'click');
isolateScope.selectFile();
scope.$digest();
expect(mockClick).calledOnce;
});
});
You can ignore all of the bard references - it's a helper library, which reduces some boilerplate. The important parts are creating the isolateScope in the beforeEach and referencing the directive's method (in this case, selectFile) on the isolateScope in the test itself. Also, notice the scope.$digest() after calling the method. Hope it helps!