We have a directive that has an optional attribute on it. If the attribute is not there, it provides a default value. The attribute is usually set from data in the scope (i.e., the value of the attribute is usually an expression and not a literal string. See http://jsfiddle.net/8PGZ4/
As such, we are using attrs.$observe in the directive to set up the scope properly. This works great in the app itself. However, when trying to test this (using Jasmine), the function in the $observe never gets run. Our test looks something like this:
describe("myDirective", function(){
function getDirectiveScope(compile, rootScope, directiveHTML)
{
return (compile(directiveHTML)(rootScope)).scope();
}
describe("foo", function () {
it("should return the default value", inject(function ($compile, $rootScope) {
var directiveScope = getDirectiveScope($compile, $rootScope, '<div my-directive></div>');
expect(directiveScope.bar).toBe("No Value");
}));
it("should now return the value given", inject(function ($compile, $rootScope) {
$rootScope.foo = "asdf";
var directiveScope = getDirectiveScope($compile, $rootScope, '<div my-directive foo="{{foo}}"></div>');
expect(directiveScope.bar).toBe("asdf");
}));
});
});
These then fail with the following errors:
Expected undefined to be 'No Value'.
Expected undefined to be 'asdf'.
I put a console.log in the $observe function to try to see what was going on, and when the tests run, I never see the log. Is there an explicit call I have to make for the $observe to run? Is there something else going on here?
You need to trigger a digest cycle to let the AngularJS magic happen in your test. Try:
it("should return the default value", inject(function ($compile, $rootScope) {
var directiveScope = getDirectiveScope($compile, $rootScope, '<div my-directive></div>');
$rootScope.$digest();
expect(directiveScope.bar).toBe("No Value");
}));
Related
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();
I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.
In my controller, I have:
var timespanServiceFn;
timespanServiceFn = function() {
return timespanService.getTimespan();
};
$scope.$watch(timespanServiceFn, $scope.updateVolume());
My $scope.updateVolume() function just has a console.log so that I know I got there.
My timespanService is also super simple:
myApp.service('timespanService', [
'$http', '$q', function($http, $q) {
var currentTimespan;
currentTimespan = '3M';
this.getTimespan = function() {
return currentTimespan;
};
this.setTimespan = function(timespan) {
console.log('setting timespan to', timespan);
return currentTimespan = timespan;
};
}
]);
So why is it that when I changed the timespan, the $watch doesn't get triggered?
The watch takes two functions as arguments. You must pass the second parameter as argument but you just called it there:
$scope.$watch(timespanServiceFn, $scope.updateVolume());
so the return statement of $scope.updateVolume() will be passed to $watch function instead of the function definition itself.
Just change it to:
$scope.$watch(timespanServiceFn, $scope.updateVolume);
See demo
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/
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.