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
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");
}
};)
My directive looks like this (removes non-digits from input):
'use strict';
angular.module('fooApp')
.directive('percent', function () {
return {
require: 'ngModel',
link: function (scope, element, attr, ngModelCtrl) {
function fromUser(text) {
// get rid of non-number data
var transformedInput = text.replace(/[^0-9]/g, '');
if(transformedInput !== text) {
ngModelCtrl.$setViewValue(transformedInput);
ngModelCtrl.$render();
}
return transformedInput;
}
ngModelCtrl.$parsers.push(fromUser);
}
};
}
my test looks like this:
'use strict';
describe('Directive: percent', function () {
var $compile;
var $rootScope;
beforeEach(module('fooApp'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$rootScope = _$rootScope_;
$compile = _$compile_;
}));
it('Removes non-digits', function () {
$compile('<input percent ng-model="someModel.Property" value="1a2b3"></input>')($rootScope);
$rootScope.$digest();
console.log($rootScope.someModel);
expect($rootScope.someModel.Property).toEqual('123');
});
});
However I can see in the log:
LOG: undefined
So it sounds like someModel is not set => test fails.
Not sure what's wrong with my directive. If I test manually: input non-digits in HTML page, these are not shown (are ignored).
What would be the proper way to test that?
I suspect, that my directive is not modifying stuff once data don't come from user but are set via value. Not sure however how to change that.
What you should do is to use the modelController $setViewValue(value) to trigger your parser pipeline. To get hold of the modelcontroller, you would have to wrap the dummy html in a form:
$compile('<form name="form1"><input percent ng-model="someModel.Property" name='data'></input></form>')($rootScope);
$rootScope.form1.data.$setViewValue('1a2b3')
$rootScope.$digest();
console.log($rootScope.someModel);
expect($rootScope.someModel.Property).toEqual('123');
Can you give it a try.
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.
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!
I want to have a call back function on a directive I wrote, without creating an isolated scope. The directive simply makes the element resizeable, and thus doesn't need a separate scope (and it's desirable NOT to have isolated scope in this case, I think).
What is the best way to do this? I tried,
.controller("MyController", function ($scope) {
$scope.myFunc = function () {
console.log("test");
};
})
.directive('resizeable', function ($document, $parse) {
return function (scope, element, attr) {
var func = $parse(attr.onFunc);
}
}
with
<div class="main" resizeable="" on-Func="myFunc()">
How to do this?
Try this JSFiddle.
You're close, here is what I'd do:
.directive('resizeable', ['$document', '$parse', function ($document, $parse) {
return {
link: function ($scope, $element, $attrs) {
var func = $parse($attrs.resizeable);
// do stuff...
func($scope);
}
};
}]);
And the HTML would be:
<div data-resizeable="myFunc()"></div>
Here is a working demo: http://plnkr.co/edit/as6Tylj0zPpjVsUaDDC6?p=preview
on-Func="myFunc"
Without parenthesis. And in the directive:
var func = scope[attr.onFunc];
The directive is in the same scope as the controller. So you can directly refernece the function.