How can I trigger `$on` events in AngularJS Karma unit tests? - angularjs

I am attempting to trigger the $scope.$on() method in a controller that is fired when I $rootScope.broadcast() an event. I found this question useful: How can I test events in angular?, but I'm still having trouble detecting an event being broadcast from the $rootScope up through a controller's $scope.
So far I've managed to test that the $broadcast method is called on a corresponding $rootScope, but not that the $on method was called on a corresponding $scope when $broadcast is called on the $rootScope.
I attempted to $rootScope.$broadcast directly in my test, but my spy is not picking up on the event.
This is my controller:
angular.module('app.admin.controllers.notes', [])
.controller('NotesCtrl', function($scope) {
$scope.$on('resource-loaded', function(event, resource) { // I want to test this
$scope.parentType = resource.type;
$scope.parentId = resource.id;
});
});
This is my test:
describe('The notes controller', function() {
beforeEach(module('app.admin.controllers.notes'));
var scope, rootScope, NotesCtrl;
beforeEach(inject(function($controller, $injector, $rootScope) {
rootScope = $rootScope;
scope = $rootScope.$new(); // I've tried this with and without $new()
NotesCtrl = $controller('NotesCtrl', {$scope: scope}); // I've tried explicitly defining $rootScope here
}));
it('should respond to the `resource-loaded` event', function() {
spyOn(scope, '$on');
rootScope.$broadcast('resource-loaded'); // This is what I expect to trigger the `$on` method
expect(scope.$on).toHaveBeenCalled();
});
});
And here's the plunkr. I've included a passing test of the $broadcast method for reference, mainly because I setup the tests in the same manner.
I've read quite a few questions relating to testing events in AngularJS, and it always seems to be a scoping issue. I've heard that in Karma unit testing, $rootScope and $scope are the same thing, but I'm not really sure what the implication is. I've tried defining the $rootScope and the $scope as the same object, as well as explicitly injecting the $rootScope into the NotesCtrl during testing, but nothing makes my test go green.
How can I get the $on method in my NotesCtrl to fire for this test?

What makes it not working is the fact that you're spying the $on function. It works fine when not sying it: http://plnkr.co/edit/hNEj7MmDDKJcJ7b298OB?p=info. And the reason is actually simple. When an event is brodcasted, what is called is not the $on() function. What is called is the callback function passed as argument to $on() previously: the listener.
Note that, by spying the $on function, you're not testing your code here. All you're trying to test is that when broadcasting en event, child scopes receive it. So you're testing AngularJS itself.

Try to use
$rootScope.$emit('resource-loaded');
Works fine in my tests.

#kirill.buga
Using $broadcast is right, not $emit because :
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
Dispatches an event name upwards through the scope hierarchy notifying the registered $rootScope.Scope listeners.
Problem of #ben-harold is trying to spy $on instead of the result of code in the $on.

Related

How to unit test service depending on $controller service in angularjs

I have a service depending on $controller service. Inside of this service, $controller service takes a controller name and locals to instantiate this controller.
When I unit test this service, I would like to pass a dummy controller name, so I can test this service properly.
From reading angular.js source code, I know $controller service looks for registered controllers by controller name. Controllers are registered through register method of $controllerProvider. How can I access this method in unit test. I'm using Jasmine here for unit testing.
Any advices are appreciated.
If you are trying to test Angular's $controller service, and assuming you are using ngMocks, you can use $controller normally. In Angular's documentation, there's an example on how to use it.
Anyway, here's the sample from the docs:
describe('myDirectiveController', function() {
it('test case', inject(function($controller) {
// Your code goes here
});
});
beforeEach(function () {
module('myApp', function ($controllerProvider) {
$controllerProvider.register(dummyController, function () { });
});
});
I ended up with this. when my service call $controller(dummyController, controllerlocals), it was able to find this dummyController registered in its local variable controllers.
Hope this would help people have the same scenario

$stateChangeSuccess always being triggered in Unit Test

In my controller I've got two event listeners on the $scope, one for $stateChangeSuccess and one for a message event:
$scope.$on('$stateChangeSuccess', function () {
$scope.methodA();
});
$scope.$on('message', function () {
$scope.methodB();
});
And in my unit test I only want to test that methodB has been called:
spyOn($scope, '$on').and.callThrough();
spyOn($scope, 'methodB');
$scope.$emit('message');
expect($scope.methodB).toHaveBeenCalled();
The problem is that $stateChangeSuccess is also being called, even though I don't emit the event myself. Should this be the case or have I misconfigured something in ui-router?
I can get around this by mocking the $stateChangeSuccess event, but I'd rather it not be called at all in my unit tests unless I emit the event myself.
Any ideas?
I managed to solve this using the second answer from this question.
Basically, you can disable ui-router in your unit tests by calling deferIntercept() on the $urlRouterProvider module.
beforeEach(module(function($urlRouterProvider) {
$urlRouterProvider.deferIntercept();
}));
This and other ui-router related issues with karma/jasmine are explained here.

Angular JS Test driven Development with multiple controllers

I have developed an web application using Angular JS. I am getting few additional CR which needs to implemented in using TTD approach. We have return unit test cases using Jasmine and Karma. The challenge currently we face is when we try to write unit test case for multiple controllers. I have a main page return on Home Controller & its has an broadcast event in another controller. When i write a unit test case Object for the controller which has this broadcast event is not initialized.
Is there any way to inject the second controller as a dependent object. Answers with Reference Sample link or demo code is much appreciated.
You state you are using Jasmine and Karma so I assume you are unit testing. If you are "unit" testing you should test each controller individually while mocking, spy, all injected services.
beforeEach(inject(function ($rootScope, $controller) {
rootScope = $rootScope;
scope = $rootScope.$new();
controller = $controller('MyCtrl as ctrl', {
'$scope': scope
});
}));
it('', function(){
//Arrange
controller.counter = 0; // Your controller is listening on scope.$on to update this counter.
//Act
rootScope.$broadcast('xyz', {});
//Assert
expect(controller.counter == 1).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 2).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 3).toBe(true);
});
Just be careful with broadcast. Only a domain events (model updated/deleted/created), or something global (signin,signout) should travel over $broadcast. Otherwise, it should be replaced with a service + directive. An example is angular material https://material.angularjs.org/latest/api/service/$mdDialog that is 1 directive with a backing service that can be open/closed from anywhere.
You can inject any controller with the $controller service, e.g.
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {
'$scope': scope
});
}));
see docs here:
https://docs.angularjs.org/api/ngMock/service/$controller
so what you do is inject that controller first, then your other controller. then the first controller will have been instantiated at the time the second gets instantiated.
I am new to angular, it seems to be possible to inject multiple controllers at once. However best practice is to generate a mock controller that behaves as you expect the second controller to behave. This reduces the number of things you are testing at once.
The following link may be helpful for creating a mock controller, http://www.powdertothepeople.tv/2014/08/28/Mocking-Controller-Instantiation-In-AngularJS-Unit-Test/ .

jasmine test requires $window to be defined

I'm running jasmine tests with gulp task and I want to test a directive. Directive itself is a simple toggle button component and requires no dependencies. So, I'm following this tutorial, starting with
beforeEach(module('app'));
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
}));
it('does something', function() {
expect(scope).toBeDefined();
});
But the test fails with this obscure log
TypeError: 'undefined' is not an object (evaluating '$window.PUBLISH_SETTINGS.webApiUrl')
Somehow, angular calls seemingly unconnected piece of code from inside my application, which requires $window to be defined. What is happening here? How do I isolate my directive so I won't need to mess with $window?

Angularjs Controller destructor

I have an AngularJs app. I use Controllers for some child scopes. In every Controller I can set a number of variables that belong to the corresponding Child Scope. When AngularJs instantiate a controller, there is a constructor where I can set a default value to my child-scope variables.
Do I have a controller "destructor"? How do I know when a controller is closing and the scope is being cleaned (destroyed by the $destroy function)?
Thanks!
You have to listen to the $destroy event, e.g.:
function MyController($scope, ...) {
...
$scope.$on("$destroy", function handler() {
// destruction code here
});
}
Relevant docs: https://docs.angularjs.org/api/ng/type/$rootScope.Scope

Resources