Unit testing Angular directive click handler - javascript

I've got a directive that adds a click handler to an element:
module.directive('toggleSection', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind('click', function (event) {
scope.$apply(function () {
var scopeProp = 'show' + attrs.toggleSection;
event.preventDefault();
event.stopPropagation();
scope[scopeProp] = !scope[scopeProp];
return false;
});
});
}
};
}]);
When the element is clicked, it toggles another property on the scope, which another element is bound to with ng-show. It's working as it should in the app.
I've added the following test for the directive:
(function () {
'use strict';
// get the app module from Angular
beforeEach(module('app'));
describe('myCtrl', function () {
var $scope, $rootScope;
beforeEach(inject(function ($controller, _$rootScope_) {
$scope = {};
$controller('myCtrl', { $scope: $scope });
$rootScope = _$rootScope_;
}));
describe('the toggleSection directive', function () {
var testElement;
beforeEach(function () {
testElement = $compile('<a toggle-section="Test" href="#">Collapse section</a>')($rootScope);
$rootScope.$digest();
});
it('inverts the value of the specified scope property', function () {
$scope.showTest = false;
testElement.click();
expect($scope.showTest).toEqual(true);
});
});
});
In the real code there are properties like $scope.showSection1 = false and by adding console logs in the directive I can see the properties before and after clicking the bound element and they have the expected values (e.g. the property starts as false and after you click the toggle element once it changes to true).
However, the test always fails with 'Expected false to equal true'. I think it's to do with the $apply method, because none of the show properties seem to exist on the scope when I run the test.
Other tests I have (even in the same spec file), which don't use the directive can see properties on the scope just fine.
What am I doing wrong?

There are a few things to be changed in your test:
1 - scope creation should be changed from $scope = {} into $scope = $rootScope.$new();
2 - the directive should be compiled not into rootScope, but into scope
3 - the directive should first be created via angularjs.element and then compiled:
element = angular.element('<my-directive/>');
compile(element)(scope);
scope.$digest();

Related

AngularJS - $q undefined in a directive

I'm trying to inject $q into my directive, but though $q is defined as a resolver() at first, when calling the function it is undefined. Maybe something related to binding? I don't know.
(function () {
'use strict';
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
restrict: 'EA',
scope: {
ngSubmitFunction: '&',
},
templateUrl: 'myTemplate',
controllerAs: 'ctrl',
controller: ["$scope", "$window", "$q", function ($scope, $window, $q) {
var vm = this;
vm.name = 'myForm';
$scope.submitPromise = function(){};
vm.ngSubmit = ngSubmit;
function ngSubmit($form) {
vm.submitDisabled = true;
$form.$setSubmitted();
if ($form.$valid) {
$scope.submitPromise().then(function() {
vm.submitDisabled = false;
});
}
}
}],
link: function (scope, element, attrs) {
console.log($q);
scope.submitPromise = function($q) {
console.log($q);
var deferred = $q.defer();
scope.ngSubmitFunction();
return deferred.promise;
}
}
};
}
}());
The objective is to call ngSubmit when user clicks on a button. ngSubmit disables the button, waits for the async calls to be over and then enables the button.
In the example code, the 1st console.log($q) (executed when loading the page) outputs this:
Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}...
Which to me looks like correct.
But when calling submitPromise() after pressing the button, this is the output:
undefined
TypeError: Cannot read property 'defer' of undefined
When is $q lost?
Note: this is not the only version I tried, originally all code was on controller, nothing on link. I've also been told this pattern is deprecated and to use this one, which is better:
function submitPromise($q) {
return $q(function (resolve) {
$scope.ngSubmitFunction();
})
}
Nothing worked. Everything produces the same error, $q gets undefined at some point and can't find out why.
Using $q as an argument parameter is causing $q to become undefined.
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
var deferred = $q.defer();
//scope.ngSubmitFunction();
deffered.resolve(scope.ngSubmitFunction());
return deferred.promise;
}
}
The myForm function is a directive construction function to which the AngularJS framework will inject service providers. But the submitPromise function is not injectable; it is a child function of myForm. All injections should be done in the parent function.
Also the code can be simplified by using $q.when to create a promise.
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
return $q.when(scope.ngSubmitFunction());
}
}
You should solve with this different injection
(function () {
'use strict';
angular
.module('myModule')
.directive('myForm', ['$q', function($q){
return {
. . .
}
}]);
Hope I've been helpful.
The right code is ( I removed parameter from function declaration ):
$scope.submitPromise=function() {
return $q(function (resolve) {//$q is available in function declared in the same scope
$scope.ngSubmitFunction();
});
}
Above code use $q variable from scope ( javascript scope not angular $scope ), $q is visible for all functions declared inside myForm function.
Your previous code used function parameter not $q from scope, parameter was not passed, so was undefined.
Javascript scope means everything between open tag { and close tag }. Check this example:
function(y){//scope start
var x; //scope local variable
var someFunc=function(){
//here is available y and x variables
};
//scope end
}
//outside of scope - here variables x and y not exists

Angular Parent Scope Variable not changing

I have a parent controller where I set instantiate an object called links. I assign a property with a value that I want to change within another function. However when I set the variable in the instagramModel the links.imagesa doesn't get updated.
I print the value out in the console and the parentscope doesn't get updated. I have thought I followed the rules of prototypical inheritance.
Why is $scope.links.imagesa not updating?
.controller('HomeCtrl', function HomeController($scope, titleService, config, $sails, $timeout, $upload, leafletData, $modal, $log) {
$scope.links = {};
$scope.links.imagesa = "This should change";
$scope.instagramModal = function (size) {
var modalInstance = $modal.open({
templateUrl: 'instagramModal.html',
controller: 'InstagramModalInstanceCtrl',
size: size,
resolve: {
items: function () {
return $sails.get("/instagram/self").success(function (response) {
return response.data;
}).error(function (response) {
console.log('error');
});
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.links.imagesa = "wept";
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
$scope.ask = function () {
console.log($scope.links.imagesa);
};
});
If you want the parent's scope to get updated, then you must use $scope.$parent.links.imagesa since the changes made in child scope are not reflected in the parent scope directly.
I had the HomeCtrl instantiated in the UI Router and also on the template page using ng-controller. This messed up the scope.
Angular UI's modals use $rootScope by default. See documentation at "http://angular-ui.github.io/bootstrap/#/modal"
You can pass a scope parameter with a custom scope when you open the modal – e.g. scope: $scope if you want to pass the parent scope. The modal controller will create a sub-scope from that scope, so you will only be able to use it for your initial values.
Hence, if you want to update any value, keep the object/data in rootScope.

Karma unit test 'undefined' is not a function error

I have a custom directive for checking if values in two input fields are equal to each other (repeat password) which I want to test. It works fine but test fails with this error:
TypeError: 'undefined' is not a function (evaluating 'element.add(tween)')
The directive is very simple and straightforward:
angular.module('fmAppApp')
.directive('valueMatch', function () {
return {
require: 'ngModel',
restrict: 'A',
link: function postLink(scope, element, attrs, ctrl) {
var tween = '#' + attrs.valueMatch;
element.add(tween).on('keyup', function () {
scope.$apply(function () {
var v = element.val() === $(tween).val();
ctrl.$setValidity('valueMatch', v);
});
});
}
};
});
And here is the spec:
describe('Directive: valueMatch', function () {
// load the directive's module
beforeEach(module('fmAppApp'));
var element, origElement,
scope;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
}));
it('should set change validity state of element depending on value of another element',
inject(function ($compile) {
origElement = $compile('<input id="orig">')(scope);
element = $compile('<input value-match="orig" ng-model="repeat">')(scope);
scope.$digest();
expect(element.$valid).toBe(true);
origElement.val('12345');
scope.$digest();
expect(element.$valid).toBe(false);
element.val('12345');
expect(element.$valid).toBe(true);
}));
});
So apparently add() is undefined, right? But why?
I believe in case of your unit test, Angular is loading jqLite instead of jQuery, which does not have add function.
Either use one of the jqlite compatible function described here https://code.angularjs.org/1.2.9/docs/api/angular.element
Or include jQuery too in your tests, in karma config.

AngularJS+Jasmine - Get a controller scope write in a directive

I'm kinda new in AngularJS unit testing and I'm having some troubles to test a controller method that was written in a directive.
This is my directive.js
app.directive('formLogin', ['AuthService', function(AuthService){
return {
restrict: 'E',
templateUrl: utils.baseUrl + 'partials/_home-form-login.html',
replace: true,
controller: function ($scope, $element, $http, $location) {
$scope.visible = false;
$scope.showForm = function () {
$scope.visible = !$scope.visible;
};
}
};
}]);
And here goes my unit-test.js
describe('formLogin ctrl', function () {
var element, scope, compile;
beforeEach(module('Application'));
beforeEach(inject(function ($rootScope, $compile) {
element = angular.element('<form-login></form-login>');
scope = $rootScope.$new();
compile = $compile(element)($scope);
}));
it('Test', function() {
expect(scope.visible).toBe(false);
})
});
And by doing this the "scope.visible" come as undefined.
There are some way to take the $scope from my controller that assumes in "scope" variable the "visible" property and the "showForm" method?
From this link
it looks like you might need to do a scope.$digest();
You appear to have a couple problems:
compile = $compile(element)($scope); -- here, $scope is undefined. It should be compile = $compile(element)(scope);
As mentioned by smk, you need to digest your scope to finish the directive creation process
This is especially important because you are using templateUrl. When you just use a locally-defined template, as Krzysztof does in his example, you can get by with skipping this step.
You will probably notice that when you add scope.$digest() you will get a different problem about an unexpected GET request. This is because Angular is trying to GET the templateUrl, and during testing all HTTP requests must be configured / expected manually. You might be tempted to inject $httpBackend and do something like $httpBackend.whenGet(/partials\//).respond('<div/>'); but you will end up with problems that way.
The better way to accomplish this is to inject the template $templateCache -- Karma has a pre-processor to do this for you, or you can do it manually. There have been other StackOverflow questions you can read about this, like this one.
I've modified your example to manually insert a simple template into the $templateCache as a simple example in this plunkr.
You should take a look into Karma's html2js pre-processor to see if it can do the job for you.
If your directive hasn't isolated scope, you can call methods from directive controller and test how it impacts to scope values.
describe('myApp', function () {
var scope
, element
;
beforeEach(function () {
module('myApp');
});
describe('Directive: formLogin', function () {
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('<form-login></form-login>');
$compile(element)(scope);
}));
it('.showForm() – changes value of $scope.visible', function() {
expect(scope.visible).toBe(false);
scope.showForm();
expect(scope.visible).toBe(true);
});
});
});
jsfiddle: http://jsfiddle.net/krzysztof_safjanowski/L2rBV/1/

Why is my $watch not working in my directive test - Angularjs

I have a directive in which I pass in an attrs and then it is watched in the directive. Once the attrs is changed, then an animation takes place. My attrs always is undefined when the $watch gets triggered.
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.resize, function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
});
};
});
And here is my test:
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.attr('resize', 'true');
element.scope().$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
When the attrs changes, my watchers newValue is undefined and so nothing happens. What do I need to do to make this work? Here is a plunker
You are not watching the value of attrs.resize; you are watching the value pointed by attrs.resize instead, in the test case a scope member called isHidden. This does not exist, thus the undefined.
For what you aare trying to do, the following would work:
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(
// NOTE THE DIFFERENCE HERE
function() {
return element.attr("resize");
// EDIT: Changed in favor of line above...
// return attrs.resize;
},
function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
}
);
};
});
EDIT: It seems that the attrs object does NOT get updated from DOM updates for non-interpolated values. So you will have to watch element.attr("resize"). I fear this is not effective though... See forked plunk: http://plnkr.co/edit/iBNpha33e2Xw8CHgWmVx?p=preview
Here is how I was able to make this test work. I am passing in a variable as an attr to the directive. The variable name is isHidden. Here is my test with the updated code that is working.
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.scope().isHidden = true;
scope.$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
I am able to access the variable isHidden through the scope that is attached to the element. After I change the variable, the I have to run $digest to update and then all is golden.
I feel that I should probably be using $observe here as was noted by package. I will look at that and add a comment when I get it working.
As Nikos has pointed out the problem is that you're not watching the value of attrs.resize so what you can try doing is this:
Create a variable to hold your data and create these $watch functions:
var dataGetter;
scope.$watch(function () {
return attrs.resize;
}, function (newVal) {
dataGetter = $parse(newVal);
});
scope.$watch(function () {
return dataGetter && dataGetter(scope);
}, function (newVal) {
// Do stuff here
});
What should happen here is that Angular's $parse function should evaluate attrs.resize and return a function like this. Then you pass it the scope and do something. As long as attrs.resize is just a boolean then newVal in the 2nd watch expression should be a boolean, I hope.

Categories

Resources