Mocked function not found in AngularUI modal controller unit test - angularjs

I'm using Angular-Bootstrap modals and have a basic controller like so:
.controller('DashboardHelpController', ['$scope', '$uibModal', function ($scope, $uibModal) {
var dhc = this;
dhc.open = function (size, resource) {
var modalInstance = $uibModal.open({
templateUrl: 'resourcePlayModal.html',
controller: 'ModalInstanceCtrl as mic',
size: size,
resolve: {
resource: function () {
return resource;
}
}
});
};
}])
It calls a standard modal instance controller:
.controller('ModalInstanceCtrl', ['$uibModalInstance', 'resource', function ($uibModalInstance, resource) {
this.resource = resource;
this.cancel = function () {
$uibModalInstance.dismiss();
};
}])
And here's my unit test, modeled after another SO post:
describe('Modal controller', function () {
var modalCtrl, scope, modalInstance;
beforeEach(module('MyApp'));
// initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
modalInstance = {
// create a mock object using spies
close: jasmine.createSpy('modalInstance.close'),
dismiss: jasmine.createSpy('modalInstance.dismiss'),
result: {
then: jasmine.createSpy('modalInstance.result.then')
}
};
modalCtrl = $controller('DashboardHelpController', {
$scope: scope,
$uibModalInstance: modalInstance
});
}));
it('should instantiate the mock controller', function () {
expect(modalCtrl).not.toBeUndefined();
});
it('should have called the modal dismiss function', function () {
scope.cancel;
expect(modalInstance.dismiss).toHaveBeenCalled();
});
});
The problem is that the cancel function isn't found on the scope:
Expected spy modalInstance.dismiss to have been called with [ 'cancel'
] but it was never called.
UPDATE: I had originally attempted to call cancel as a function:
it('should have called the modal dismiss function', function () {
scope.cancel();
expect(modalInstance.dismiss).toHaveBeenCalled();
});
That didn't work. My code above is an attempt to solve the original problem:
TypeError: scope.cancel is not a function
Things are complicated a bit my my use of the Controller as syntax, but this should work. Thanks for any help.

scope.cancel is a function, but you aren't invoking it as such.
it('should have called the modal dismiss function', function () {
scope.cancel();
expect(modalInstance.dismiss).toHaveBeenCalledWith();
});
In addition, scope.cancel() is never defined on DashboardHelpController, even though that is the scope that you create for your tests. You need to create a method on your dashboard controller that calls through to the modal instance close method after the modalInstance has been created.
var modalInstance = $uibModal.open({
...
dhc.cancel = function () {
modalInstance.dismiss();
}

Related

How to deal with modal controller outside AngularJs module?

My app uses Angular UI to deal with modals and I think I found a problem, as below:
I have a controller that calls the modal controller:
angular.module('MyApp').controller('MainCtrl', function ($scope, $modal) {
$scope.openModal = function () {
var myModal = $modal.open({
animation: true,
templateUrl: 'ModalContent.html',
controller: ModalCtrl
});
};
});
I also have the modal controller in another file, as a simple function:
function ModalCtrl ($scope, $modalInstance) {
$scope.ok = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
It's working well, but I think the modal controller have to be inside AngularJs module. The questions are:
Can I do it and keep the modal controller in a separated file?
It's a good practice keep the modal controller outside AngularJs module?
What is the best practice to reuse a modal controller in many pages?
Thanks a lot!
That object you're passing into $modal.open() can be registered as a shared service and the modal controller can be embedded right inside it.
angular.module('myApp').factory('myModalConfig', function() {
return {
animation: true,
templateUrl: 'ModalContent.html',
controller: function($scope, $modalInstance) {
$scope.ok = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
}
};
});
Then just inject that along with the $modal service and pass it.
angular.module('MyApp').controller('MainCtrl', function ($scope, $modal, myModalConfig) {
$scope.openModal = function () {
var myModal = $modal.open(myModalConfig);
};
});
I go further than this myself and wrap the whole thing into a custom modal service factory, so I can just inject one thing and open modals like this: profileModals.editProfile(). But that's beyond the scope of this answer.

Testing modalInstance.result.then

I am finally working on learning how to test using an older angularjs app I wrote. I have a few modals in my controller and I cannot figure out for the life of me how to make sure the code in 'modalInstance.result.then' is run and test against it.
I've searched Google and SO and have found examples of people testing their modals but so far they all seem to involve testing the modal controller itself.
How to I get that promise (modalInstance.result.then) to resolve? I've tried running $modal.close() but that fails to an error. I've tried mocking modalInstance and $modal in a number of ways, using jasmine spys, etc. My ignorance when it comes to testing is holding me up. Any help would be appreicated.
Here is my controller.js:
(function() {
var comment = angular.module('APP.comment', ['APP.user']);
var commentController = function($scope, $modal) {
var self = this;
self.addComment = function(newComment) {
var modalInstance = $modal.open({
templateUrl: 'views/commentModal.html',
backdrop: 'static',
windowClass: 'modal',
controller: 'commentModalController',
controllerAs: 'commentCtrl',
resolve: {
newComment: function() {
return newComment;
}
}
});
modalInstance.result.then(function(data) {
// How do I test that the function or branches here
// were run?
if (data.length === 2) {
// do this thing
} else {
// do this other thing
}
});
};
};
commentController.$inject = ['$scope', '$modal'];
comment.controller('commentController', commentController);
}());
Here is the test I have so far:
describe('Unit: commentController', function() {
var $rootScope,
$scope,
$controller,
$modal;
beforeEach(module('APP.comment'));
beforeEach(inject(function(_$rootScope_, _$controller_, _$modal_) {
$modal = _$modal_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_('commentController as commentCtrl', {
$scope: $scope,
$modal: $modal,
});
}));
it('should have controller defined', function() {
expect($scope.qaCtrl).toBeDefined();
});
it('should have method defined', function() {
expect($scope.qaCtrl.addComment).toBeDefined();
});
describe('$scope.commentCtrl.addComment', function() {
it('should open modal', function() {
$scope.commentCtrl.addComment();
});
});
});
I have a plnkr here:
http://plnkr.co/edit/YtYVPReH9yysZXPjbsC0?p=preview
it('should open modal', inject(function($q) {
var fakeResult = {
result: $q.when([])
};
spyOn($modal, 'open').and.returnValue(fakeResult);
$scope.commentCtrl.addComment();
$scope.$apply();
// now check that the right thing has been done, given the empty array returned
}));

Testing Bootstrap modal in Angular controller

I have been trying to find a way of testing this controller part for a few days but keep getting stuck. Now I get a ReferenceError: Can't find variable: $modal but I have it injected so im not sure why its not working. I also know that this test I am writing doesn't really test anything important so if you have any suggestions about moving forward please let me know. And thank you to anyone who has helped me on code throughout this controller
Code:
$scope.confirmDelete = function (account) {
var modalInstance = $modal.open({
templateUrl: '/app/accounts/views/_delete.html',
controller: function (global, $scope, $modalInstance, account) {
$scope.account = account;
$scope.delete = function (account) {
global.setFormSubmitInProgress(true);
accountService.deleteAccount(global.activeOrganizationId, account.entityId).then(function () {
global.setFormSubmitInProgress(false);
$modalInstance.close();
},
function (errorData) {
global.setFormSubmitInProgress(false);
});
};
$scope.cancel = function () {
global.setFormSubmitInProgress(false);
$modalInstance.dismiss('cancel');
};
},
resolve: {
account: function () {
return account;
}
}
});
Test:
describe("confirmDelete() function", function () {
var controller, scope;
// sets scope of controller before each test
beforeEach(inject(function ($rootScope, _$modal_) {
scope = $rootScope.$new();
controller = $controller('AccountsController',
{
$scope: scope,
$stateParams: mockStateParams,
$state: mockState,
// below: in order to call the $modal have it be defined and send on the mock modal?
$modal: _$modal_,
//modalInstance: mockModalInstance,
global: mockGlobal,
accountService: mockAccountSrv
});
}));
beforeEach(inject(function ($modal, $q) {
spyOn($modal, 'open').and.returnValue({
result: $q.defer().promise
});
}));
it("make sure modal promise resolves", function () {
scope.confirmDelete(mockAccountSrv.account);
expect($modal.open).toHaveBeenCalled();
});
});
You need to set modal to a variable in order to be able to use it.
i.e
describe("confirmDelete() function", function () {
var controller, scope, $modal; //Initialize it here
//....
beforeEach(inject(function ($rootScope, _$modal_, $controller) {
$modal = _$modal_; //Set it here
And you need to inject $controller as well in order to be able to use it.
Plnkr

Jasmine Testing Angular Controller Method

So I have my blank tests passing with this setup.
describe('loginController', function() {
var scope, createController;
beforeEach(module('souply'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
$location = _$location_;
scope = $rootScope.$new();
createController = function() {
return $controller('loginController', {
'$scope': scope
});
};
}));
And here are the tests...
describe('processGoogleLogin', function(){
describe('successful', function(){
beforeEach(function() {
});
it('should connect to google plus', function () {
expect(true).toBe(true);
});
This one passes no problem.
QUESTION: How can I test a method on the login controller?
Here is the method I want to test on the login controller:
$scope.processGoogleLogin = function(){
console.log('process login was clicked');
window.location.replace('/#/dashboard');
};
The test I have so far is:
it('should sign you into the dashboard', function () {
scope.processGoogleLogin();
//$controller.processGoogleLogin();
//expect(window.location).toBe('/#/dashboard');
});
This test throws an error of:
'undefined' is not a function (evaluating 'scope.processGoogleLogin()')
This needed this line.
var ctrl = $controllerConstructor('myController', {$scope: scope, myResolve: {}, state: state});

Unit testing controller which uses $state.transitionTo

within a controller i have a function which uses $state.transitionTo to "redirect" to another state.
now i am stuck in testing this function, i get always the error Error: No such state 'state-two'. how can i test this? it its totally clear to me that the controller does not know anything about the other states, but how can i mock this state?
some code:
angular.module( 'mymodule.state-one', [
'ui.state'
])
.config(function config($stateProvider) {
$stateProvider.state('state-one', {
url: '/state-one',
views: {
'main': {
controller: 'MyCtrl',
templateUrl: 'mytemplate.tpl.html'
}
}
});
})
.controller('MyCtrl',
function ($scope, $state) {
$scope.testVar = false;
$scope.myFunc = function () {
$scope.testVar = true;
$state.transitionTo('state-two');
};
}
);
describe('- mymodule.state-one', function () {
var MyCtrl, scope
beforeEach(module('mymodule.state-one'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
MyCtrl = $controller('MyCtrl', {
$scope: scope
});
}));
describe('- myFunc function', function () {
it('- should be a function', function () {
expect(typeof scope.myFunc).toBe('function');
});
it('- should test scope.testVar to true', function () {
scope.myFunc();
expect(scope.testVar).toBe(true);
expect(scope.testVar).not.toBe(false);
});
});
});
Disclaimer: I haven't done this myself, so I totally don't know if it will work and is what your are after.
From the top of my head, two solutions come to my mind.
1.) In your tests pre configure the $stateProvider to return a mocked state for the state-two That's also what the ui-router project itself does to test state transitions.
See: https://github.com/angular-ui/ui-router/blob/04d02d087b31091868c7fd64a33e3dfc1422d485/test/stateSpec.js#L29-L42
2.) catch and parse the exception and interpret it as fulfilled test if tries to get to state-two
The second approach seems very hackish, so I would vote for the first.
However, chances are that I totally got you wrong and should probably get some rest.
Solution code:
beforeEach(module(function ($stateProvider) {
$stateProvider.state('state-two', { url: '/' });
}));
I recently asked this question as a github issue and it was answered very helpfully.
https://github.com/angular-ui/ui-router/issues/537
You should do a $rootScope.$apply() and then be able to test. Note that by default if you use templateUrl you will get an "unexpected GET request" for the view, but you can resolve this by including your templates into your test.
'use strict';
describe('Controller: CourseCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
// load controller widgets/views/partials
var views = [
'views/course.html',
'views/main.html'
];
views.forEach(function(view) {
beforeEach(module(view));
});
var CourseCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
CourseCtrl = $controller('CourseCtrl', {
$scope: scope
});
}));
it('should should transition to main.course', inject(function ($state, $rootScope) {
$state.transitionTo('main.course');
$rootScope.$apply();
expect($state.current.name).toBe('main.course');
}));
});
Also if you want to expect on that the transition was made like so
expect(state.current.name).toEqual('state-two')
then you need to scope.$apply before the expect() for it to work

Resources