Testing AngularUI Bootstrap modal instance controller - javascript

This is a somewhat of a follow-on question to this one: Mocking $modal in AngularJS unit tests
The referenced SO is an excellent question with very useful answer. The question I am left with after this however is this: how do I unit test the modal instance controller? In the referenced SO, the invoking controller is tested, but the modal instance controller is mocked. Arguably the latter should also be tested, but this has proven to be very tricky. Here's why:
I'll copy the same example from the referenced SO here:
.controller('ModalInstanceCtrl', function($scope, $modalInstance, items){
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
So my first thought was that I would just instantiate the controller directly in a test, just like any other controller under test:
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
ctrl = $controller('ModalInstanceCtrl', {$scope: scope});
});
This does not work because in this context, angular does not have a provider to inject $modalInstance, since that is supplied by the UI modal.
Next, I turn to plan B: use $modal.open to instantiate the controller. This will run as expected:
beforeEach(inject(function($rootScope, $modal) {
scope = $rootScope.$new();
modalInstance = $modal.open({
template: '<html></html>',
controller: 'ModalInstanceCtrl',
scope: scope
});
});
(Note, template can't be an empty string or it will fail cryptically.)
The problem now is that I have no visibility into the scope, which is the customary way to unit test resource gathering, etc. In my real code, the controller calls a resource service to populate a list of choices; my attempt to test this sets an expectGet to satisfy the service my controller is using, and I want to validate that the controller is putting the result in its scope. But the modal is creating a new scope for the modal instance controller (using the scope I pass in as a prototype), and I can't figure out how I can get a hole of that scope. The modalInstance object does not have a window into the controller.
Any suggestions on the "right" way to test this?
(N.B.: the behavior of creating a derivative scope for the modal instance controller is not unexpected – it is documented behavior. My question of how to test it is still valid regardless.)

I test the controllers used in modal dialogs by instantiating the controller directly (the same way you initially thought to do it above).
Since there there's no mocked version of $modalInstance, I simply create a mock object and pass that into the controller.
var modalInstance = { close: function() {}, dismiss: function() {} };
var items = []; // whatever...
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
ctrl = $controller('ModalInstanceCtrl', {
$scope: scope,
$modalInstance: modalInstance,
items: items
});
}));
Now the dependencies for the controller are satisfied and you can test this controller like any other controller.
For example, I can do spyOn(modalInstance, 'close') and then assert that my controller is closing the dialog at the appropriate time.

Alternatively, if you're using jasmine, you can mock the $uibModalInstance using the createSpy method:
beforeEach(inject(function ($controller, $rootScope) {
$scope = $rootScope.$new();
$uibModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);
ModalCtrl = $controller('ModalCtrl', {
$scope: $scope,
$uibModalInstance: $uibModalInstance,
});
}));
And test it without having to call spyOn on each method, let's say you have 2 scope methods, cancel() and confirm():
it('should let the user dismiss the modal', function () {
expect($scope.cancel).toBeDefined();
$scope.cancel();
expect($uibModalInstance.dismiss).toHaveBeenCalled();
});
it('should let the user confirm the modal', function () {
expect($scope.confirm).toBeDefined();
$scope.confirm();
expect($uibModalInstance.close).toHaveBeenCalled();
});

The same problem is with $uidModalInstance and you can solve it in similar way:
var uidModalInstance = { close: function() {}, dismiss: function() {} };
$ctrl = $controller('ModalInstanceCtrl', {
$scope: $scope,
$uibModalInstance: uidModalInstance
});
or as said #yvesmancera you can use jasmine.createSpy method instead, like:
var uidModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);
$ctrl = $controller('ModalInstanceCtrl', {
$scope: $scope,
$uibModalInstance: uidModalInstance
});

Follow below given steps:
Define stub for ModalInstance like give below
uibModalInstanceStub = {
close: sinon.stub(),
dismiss: sinon.stub()
};
Pass the modal instance stub while creating controller
function createController() {
return $controller(
ppcConfirmGapModalComponentFullName,
{
$scope: scopeStub,
$uibModalInstance: uibModalInstanceStub
});
}
});
Stub methods close(), dismiss() will get called as part of the tests
it('confirm modal - verify confirm action, on ok() call calls modalInstance close() function', function() {
action = 'Ok';
scopeStub.item = testItem;
createController();
scopeStub.ok();
});

Related

How to create / instantiate AngularJs controllers on the fly?

I have a simple controller that works just fine:
app.controller('IndexController', ['$scope', obj.indexPage]);
var obj = {};
obj.indexPage = function ($scope) { // do controller stuff };
I also have an event function that i want to use to load/create/instantiate this controller:
// some callback, doesn't really matter
app.onPage('index', function () {
// load and run controller logic in here
app.controller('IndexController', ['$scope', obj.indexPage]);
}, obj);
there are some issues, like Argument 'IndexController' is not a function, got undefined
Any ideas?
my solution:
app.controller('IndexController', ['$scope', function ($scope) {
var obj = {};
obj.indexPage = function (data) {
// do controller stuff
};
app.onPage('index', function (data) {
obj.indexPage(data);
}, obj);
});
Due to how the angular module system works, you can't instantiate controllers asynchronously like that. You can however, use the $controller service to create controllers on the fly. The same technique below is often used in unit testing.
For example:
angular.module('app', [])
.controller('MyCtrl', function($rootScope, CtrlFactory){
var dynamicCtrl = CtrlFactory.create({$scope: $rootScope.$new()});
console.log(dynamicCtrl.method()); //-> 123
})
.factory('CtrlFactory', function($controller) {
return {
create: function(locals) {
return $controller(
//this is the constructor of the new controller
function($scope){
console.log('Dynamic controller', $scope);
this.method = function() { return 123; };
},
//these are the injected deps
locals
);
}
};
})
For some example usage in a unit testing context, see: https://docs.angularjs.org/guide/controller.
I'll add that you may want to reconsider your reasons for doing this--I can't say I've seen $controller used outside testing.
app.onPage('index', function () {
app.controller('IndexController', obj.indexPage); // this would load the controller to the module
$controller('IndexController', { $scope: $scope }); // This would instantiate the controller, NOTE: $controller service should be injected
}, obj);

combine two scopes in angular

I have two scopes and need to combine them. Does anyone know the best way to do this? below are the two scopes. the second one comes from a service
The first scope, retrieves info from database using PHP SLIM RESTful API
Data.get('posts').then(function(data){
$scope.posts = data.data;
});
The second scope retrieves info from database via a service
dataShare.getconfigs().then(function(data){
$scope.configs = data;
});
UPDATE:
When I open the modal to edit I only get the $scope.posts. I am not currently passing through the $scope.configs
$scope.open = function (p,size) {
var modalInstance = $uibModal.open({
templateUrl: 'views/postsEdit.html',
controller: 'postsEditCtrl',
size: size,
resolve: {
item: function () {
return p;
}
}
}); ...
The problem is that $modal.open will create new isolated scope which will be used inside of modal controller and template. This new scope is going to be a direct child of the $rootScope, and thus it will not inherit from your $scope. However, what you want is to inherit from the $scope object from which you open a modal. For this configure modal like this:
$scope.open = function(p, size) {
var modalInstance = $uibModal.open({
templateUrl: 'views/postsEdit.html',
controller: 'postsEditCtrl',
size: size,
scope: $scope, // <-- use $scope as a parent to for modal scope
resolve: {
item: function() {
return p;
}
}
});
};

Unit testing AngularJS Controller whilst following best practice

We are building an AngularJS app following some of the best practice guidelines which are outlined here.
Am specifically interested in testing a very simple controller to get up and running with karma.
The controller code is:
angular.module('ttn').controller('Login', Login);
function Login(){
var login = this;
login.title = 'foo bar content here etc';
}
And the spec code is:
describe('Controller: Login', function () {
beforeEach(module('ttn'));
var scope, controller;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('Login', {
$scope: scope
});
scope.$digest();
}));
it('should define a title', function () {
expect(scope.title).toBeDefined();
});
});
This fails with expecting undefined to be defined.
If I change the controller to:
angular.module('ttn').controller('Login', Login);
function Login($scope){
$scope.title = 'foo bar whatsit jibber';
}
The test then passes as expected. I am not sure how to reference the controller written in the manner outlined on the above link to get the test to pass.
Since your controller doesn't use $scope, you shouldn't be injecting it and using it in your tests. Instead you should be checking for title on your controller:
describe('Controller: Login', function () {
beforeEach(module('ttn'));
var controller;
beforeEach(inject(function ($controller) {
controller = $controller('Login', {});
}));
it('should define a title', function () {
expect(controller.title).toBeDefined();
});
});
Plunkr

Unit testing angular/Ionic project

I have a very simple controller that looks like this.
timeInOut.controller('timeInOutController', function($scope, $filter, $ionicScrollDelegate){
...
});
Whenever I try to create a unit test for it like so...
(function() {
'use strict';
var scope, controller, filter;
describe('timeInOutController', function () {
beforeEach(module('common.directives.kmDateToday'));
beforeEach(inject(function ($rootScope, $controller, $filter) {
scope = $rootScope.$new();
filter = $filter;
controller = $controller('timeInOutController', {
$scope: scope
});
}));
describe('#date setting', function(){
...
});
});
})();
I get the error:
[$injector:unpr] Unknown provider: $ionicScrollDelegateProvider <- $ionicScrollDelegate
Obviously in my example here I'm not trying to inject the $ionicScrollDelegate into the test, that's just because I've tried it any number of ways with no success and don't know which failed attempt to include.
Also in my karma.conf.js file I am including the ionic.bundle.js and angular-mocks.js libraries/files.
I can successfully unit test anything that doesn't use anything $ionic in it, so I know my testing framework is set up correctly, the issue is injecting anything ionic related.
You need to pass in all the parameters if you're going to instantiate your controller via angular. By adding the parameters you are telling angular that any time you create one of these controllers I need these things too because I am dependent upon them.
So my suggestion is to mock up some representation of these dependencies and inject them in when you are creating the controller. They do not have to be (and should not be) the actual services for your unit tests. Jasmine gives you the ability to create spy objects that you can inject so you can verify the the behavior of this unit.
(function() {
'use strict';
var scope, controller, filter, ionicScrollDelegate;
describe('timeInOutController', function () {
beforeEach(module('common.directives.kmDateToday'));
beforeEach(inject(function ($rootScope, $controller, $filter) {
scope = $rootScope.$new();
filter = $filter;
// func1 and func2 are functions that will be created as spies on ionicScrollDelegate
ionicScrollDelegate = jasmine.createSpyObj('ionicScrollDelegate', ['func1', 'func2']
controller = $controller('timeInOutController', {
$scope: scope,
$filter: filter,
$ionicScrollDelegate: ionicScrollDelegate
});
}));
describe('#date setting', function(){
...
});
});
})();
You can find more about spies via jasmine's documentation
You need to create mock objects for all dependencies your controller is using.
Take this controller as an example:
angular.module('app.module', [])
.controller('Ctrl', function($scope, $ionicLoading) {
$ionicLoading.show();
});
Here you are using the $ionicLoading service, so if you want to test this controller, you have to mock that object specifying the methods you're using in the controller:
describe('Test', function() {
// Mocks
var $scope, ionicLoadingMock;
var ctrl;
beforeEach(module('app.module'));
beforeEach(function() {
// Create $ionicLoading mock with `show` method
ionicLoadingMock = jasmine.createSpyObj('ionicLoading', ['show']);
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('Ctrl', {
$scope: $scope,
$ionicLoading: ionicLoadingMock
});
});
});
// Your test goes here
it('should init controller for testing', function() {
expect(true).toBe(true);
});
});

AngularJS Jasmine test: scoped variable always undefined

I am very new to testing in Javascript and am currenly trying to test a controller function.
The function calls a service method which retrieves data from a web sql db.
This is a part of my controller function (it contains 2 callbacks, one for success and another for error):
$scope.getLocations = function () {
LocationDbService.getAll(
//Success
function (tx, results) {
$scope.numberOfLocations = results.rows.length;
...
},
//Error
function () {
console.log("Error");
});
}
The test:
it('we should be able to retrieve all stored locations',
function () {
expect(scope.numberOfLocations).toBeUndefined();
scope.getLocations();
expect(scope.numberOfLocations).toBeDefined();
});
beforeEach test:
var ctrl, scope, location, locationDbService;
// inject the $controller and $rootScope services
// in the beforeEach block
beforeEach(inject(function ($controller, $rootScope, $location, LocationDbService) {
// Create a new scope that's a child of the $rootScope
scope = $rootScope.$new();
// Create the controller
ctrl = $controller('LocationsCtrl', {
$scope: scope
});
location = $location;
locationDbService = LocationDbService;
}));
Controller header:
.controller('LocationsCtrl', function ($scope, $location, LocationDbService) {
When I run the application in the browser (or on my smartphone, its a hybrid app) everything works but when I run the test I get the following:
Does somebody know why the scoped variable is still undefined?
Thanks in advance!
When instantiating your controller, you should also inject any other services it needs.
AngularJS has a cool trick btw where you can use underscores in names:
beforeEach(inject(function ($controller, $rootScope, _$location_, _LocationDbService_) {
// Create a new scope that's a child of the $rootScope
scope = $rootScope.$new();
// Create the controller
ctrl = $controller('LocationsCtrl', {
$scope: scope,
$location : _$location_,
LocationDbService : _LocationDbService_
});
location = _$location_; //thx to the underscores you could use '$location' as name instead of 'location'
locationDbService = _LocationDbService_;
}));
Next you should mock the service call:
it('should be able to retrieve all stored locations',
function () {
spyOn(locationDbService , 'getAll').andCallFake(function (success, fail) {
var results = {};
results.rows = new Array(5);
success(null, results);
});
expect(scope.numberOfLocations).toBeUndefined();
scope.getLocations();
expect(scope.numberOfLocations).toBe(5);
});
The service should have tests of its own.

Categories

Resources