I'm having a lot of trouble getting this simple test working.
I've got an $scope.$on listener in a controller that I want to test. I just want to make certain it's called after a broadcast event.
To do this, I thought the following code would work:
describe("Testing the parent controller: ", function() {
var scope, ctrl;
beforeEach(function() {
module("myApp");
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('parent-ctrl', {
$scope: scope,
});
});
});
it ("should trigger broadcast when current page updates", function() {
spyOn(scope, "$on");
scope.$broadcast("myEvent", 999);
expect(scope.$on).toHaveBeenCalled();
});
});
It doesn't (Expected spy $on to have been called.). I've dug through numerous examples:
How do I test an event has been broadcast in AngularJS?
in-angularjs
How do I test $scope.$on in AngularJS
How can I test events in angular?
unit test spy on $emit
How do I unit test $scope.broadcast, $scope.$on using Jasmine
How do I test $scope.$on in AngularJS
How can I test Broadcast event in AngularJS
and learned a lot, but for some reason I'm just not making some critical connection.
I have noticed that the $on handler does respond post-assertion, which is unhelpful. I've tried scope.$apply() and .andCallThrough() in various configurations but nothing seems to work.
How is this done?
When the event is broadcasted it is the listener function that was registered with $on that is executed, not the $on function itself.
Your current test would work for code like this, which is probably not what you have:
$scope.$on('myEvent', function () {
$scope.$on('whatever', someFn);
});
What you should be testing is whatever your registered listener function is doing.
So if you for example have:
$scope.$on('myEvent', function() {
myFactory.doSomething();
});
Test it like this:
spyOn(myFactory, "doSomething");
scope.$broadcast("myEvent");
expect(myFactory.doSomething).toHaveBeenCalled();
Related
Hello Everyone I am struggling at testing a $.on function and I am looking for any suggestions or help on this:
controller
$scope.$on("sampleFilesSelected", function(event, args) {
$scope.sampleFiles = args.newSampleFiles;
});
spec
describe('Testing a $.on', function() {
var $scope = null;
var ctrl = null;
beforeEach(module('test'));
it('should invoke myEvent when "myEvent" broadcasted', inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
$scope.$broadcast('myEvent');
expect($scope.sampleFilesSelected).toBe(true);
}));
});
error
TypeError: Unable to get property 'newSampleFiles' of undefined or null reference
undefined
You should pass a value to your event, call a $digest before your assertion :
$scope.$broadcast('myEvent', { 'newSampleFiles' : true } );
$scope.$digest();
expect($scope.sampleFilesSelected).toBe(true);
this code ...
$scope.$broadcast('myEvent');
is not passing any args and so args.newSampleFiles throws an error because args is undefined
you need to pass args - how you do that I don't know
However, I would say ... unit testing is used for testing controller code not really for testing event handling. Your example is a bit of an edge case. I would be tempted to test the event handling use E2E testing and protractor.
I would refactor as follows ...
$scope.$on("sampleFilesSelected", function(event, args) {
$scope.sampleFiles = args.newSampleFiles;
});
would become ...
$scope.myFunction = function(event, args) {
$scope.sampleFiles = args.newSampleFiles;
}
$scope.$on("sampleFilesSelected", $scope.myFunction);
and i would unit test $scope.myFunction. And leave the testing of $scope.$on to E2E protractor testing.
Hope that helps
You're making three mistakes, first one being is that you're listening/subscribing to a event named sampleFilesSelected in your controller but in your test, you're broadcasting to anyone that's listening to an event called myEvent?
Secondly, once above is fixed, you should run $scope.$digest() cycle after you trigger an event and then you can follow it up with your expect(...).
Third, as mentioned by others, you should pass the data as second arg when $broadcast ...ing!
Inside a directive of mine called by data-type-ahead I have the following, in a series of events:
$scope.test = 5;
// Bind blur event and clear the value of
element.bind('blur', function(){
$scope.test = 0;
});
I have tried a multitude of things to use in a unit test to correctly test the functionality of this blur event however I have not been successful. I have seen mention of the function triggerHandler. Here is my attempt at the unit test:
//Inject $compile and $rootScope before each test.
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$scope.test = 5
html = angular.element('<input type="text" data-type-ahead/>');
//Apply $scope to directive html.
directive = $compile(html)($scope);
//Trigger digest cycle.
$scope.$digest();
}));
it('should trigger the events bound to the blur event on the directive', function() {
html.triggerHandler('blur')
expect($scope.test).toEqual(0);
});
However this is failing because $scope.test is remaining on 5. Is it the html element is incorrect, do I need another $digest or $apply for after I trigger the event?
You have 2 ways of getting this to work. The first is adding a timeout to your method (docs):
// somewhere above add -
var originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
// in your beforeEach
beforeEach(function(){
html.triggerHandler('blur');
setTimeout(function(){
done();
}, 1000);
});
it('should trigger the events bound to the blur event on the directive',
function() {
expect($scope.test).toEqual(0);
}
);
I believe this to be "less good" practice (bad or worse is too negative a word for testing - the moment you test, you are already better :)). In general, I try to avoid testing async, because eventually my methods (a.k.a. units) are sync inside.
The "better practice" would be to write the method that changes the value like this:
// in the directive's ctrl
this.changeValue = function changeValue{
$scope.test = 0;
}
// later on set the watcher
// Bind blur event and clear the value of
$element.bind('blur', this.changeValue);
And then test the method itself instead of testing it async. You could test the $element.bind (via the spyOn(element, 'bind')) if you like to see that your ctrl/link methods create the binding.
I have code similar to this...
//controller
function getPromise = function(){
return service.getPromise();
}
$rootScope.$on('event', function(){
return getPromise();
});
I am trying to create a jasmine test for the rootScope event. I can test the function itself (if I expose it) by using a .then and checking for the result, but I can't figure out how to do it through the on call.
I saw a piece of code in a controller recently that went something like:
.controller('foobar', ['$scope', '$rootScope', function($scope, $rootScope) {
var eventHandler = $rootScope.$on('some-event', function() {
...
});
// remove eventHandler
$scope.$on('$destroy', eventHandler);
}]);
Questions:
Is executing the eventHandler "deregistration" function on $scope's $destroy event necessary?
If yes, would executing the deregistration function on $scope's $destroy event have been necessary if 'some-event' was $on $scope instead of $rootScope?
How do I know when I need to execute a deregistration function? I understand detaching or unbinding events is common for cleanup in JavaScript, but what rules can I follow to know when to do this in Angular?
Any advice about understanding this snippet/"deregistration" would be much appreciated.
In the example above the destroy method is necessary. The listener is bound to the $rootscope which means that even after the controller gets $destroy-ed the listener is still attached to the dom through the $rootscope. Every time the controller is instantiated a new eventhandler will be created so without the destroy method you will have a memory leak.
However if you bind the listener to the controllers $scope it will get destroyed along with the controller as the $scope gets destroyed so the listener has no connection to the dom thus making it eligible for garbage collection
Event handlers are only deregistered on controller's $destroy event when it is on that controller's $scope.
The deregistering would be unnecessary if it's on $scope since that's handled for you by Angular.
Generally if it's not tied to instance of the individual element, controller, or service you are listening on then that is when you need to handle deregistering yourself.
A good example is a directive that registers event listeners on the $document:
var module = angular.module('test', []);
module.directive('onDocumentClick', function directiveFactory($document) {
return {
link: function (scope, element, attrs) {
var onDocumentClick = function () {
console.log('document clicked')
};
$document.on('click', onDocumentClick);
// we need to deregister onDocumentClick because the event listener is on the $document not the directive's element
element.on('$destroy', function () {
$document.off('click', onDocumentClick);
});
}
};
});
I have a regular angular app with a directive. This directive contains an element with a ng-click="clickFunction()" call. All works well when I click that element. I now need to write a test for this click, making sure that this function was actually run when the element was clicked - this is what I'm having trouble with.
Here's a jsfiddle to illustrate my issue: http://jsfiddle.net/miphe/v0ged3vb/
The controller contains a function clickFunction() which should be called on click. The unit test should imitate a click on the directive's element and thus trigger the call to that function.
The clickFunction is mocked with sinonjs so that I can check whether it was called or not. That test fails, meaning there was no click.
What am I doing wrong here?
I've seen the answer to similar questions like Testing JavaScript Click Event with Sinon but I do not want to use full jQuery, and I believe I'm mocking (spying on) the correct function.
Here's the js from the fiddle above (for those who prefer to see it here):
angular.js, angular-mocks.js is loaded as well.
// App
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function($scope) {
$scope.person = 'Mr';
$scope.clickFunction = function() {
// Some important functionality
};
});
myApp.directive('pers', function() {
return {
restrict: 'E',
template: '<h2 ng-click="clickFunction()" ng-model="person">Person</h2>',
};
});
// Test suite
describe('Pers directive', function() {
var $scope, $controller, template = '<pers></pers>', compiled;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller, $compile) {
$scope = $rootScope.$new();
ctrl = $controller('MyCtrl', {$scope: $scope});
compiled = $compile(template)($scope);
// Do I need to run a $scope.$apply() here?
console.log($scope.$apply); // This is a function, apparently.
//$scope.$apply(); // But running it breaks this function.
}));
it('should render directive', function() {
el = compiled.find('h2');
expect(el.length).to.equal(1);
});
it('should run clickFunction() when clicked', function() {
el = compiled.find('h2');
sinon.spy($scope, 'clickFunction');
// Here's the problem! How can I trigger a click?
//el.trigger('click');
//el.triggerHandler('click');
expect($scope.clickFunction.calledOnce).to.be.true
});
});
// Run tests
mocha.run();
Turns out the problem was quite hidden.
Firstly the $scope.$digest and $scope.$apply functions broke the beforeEach function which ultimately led to the whole solution.
Solution
Do not mix angular versions.
In the first fiddle
angular.js version 1.3.0
angular-mocks.js version 1.1.5
In the solved fiddle
angular.js version 1.3.0
angular-mocks.js version 1.3.0
That was the whole problem, and gave me quite obscure errors.
Thanks to Foxandxss from the #AngularJS IRC channel on freenode.
The way to trigger events on the directive with jQlite was simply:
someElement.triggerHandler('click');