How to unit test $uibModal in Jasmine? (unit testing injected library) - angularjs

So I am using $uibModal from bootstrap and I have the following code in my AngularJS controller:
vm.openPopup = function() {
$uibModal.open({
templateUrl: 'popup.html',
controller: function() {
var modal = this;
modal.hi = function() {
// some code here
}
}
});
};
How would I go about calling the modal.hi function in Jasmine and unit testing it to make sure it works correctly?

So the main problem with testing this code is that you've basically "buried" an anonymous function (modal.hi) inside of another anonymous function ($uibModal.open). That makes it pretty tricky to test.
You've got a few options: a.) you can mock the $uibModal service, b.) you can restructure your code, or c.) you could just drop your hi function onto the vm itself, and then call it from your tests. I think the last option would be the most expedient, but here are some examples of all three approaches.
Option 1: Mock the $uibModal service
describe('Test vm.openPopup', function () {
var mockUibModal = {
open: function(options){
var ctrl = options.controller();
// call your `hi` function:
ctrl.hi();
}
};
beforeEach(function(){
module(function($provide){
$provide.value('$uibModal', mockUibModal);
});
});
});
And from there, you could call your vm.openPopup method, and go about testing the results. Note that the module function comes from angular-mocks, which you'll need to install/include with your tests. Related question: "How do you mock a service in AngularJS when unit testing with jasmine?"
Option 2: Restructure your code
Here's a pattern that I frequently use, which involves shifting the logic/functions you wish to test into a separate factory:
var app = angular.controller('YourController', function ($uibModal, MyHelperFactory) {
var vm = this;
var modal;
var helper = MyHelperFactory(vm, modal);
vm.openPopup = function () {
$uibModal.open({
templateUrl: 'popup.html',
controller: function () {
modal = this;
modal.hi = helper.hi;
}
});
};
});
app.factory('MyHelperFactory', function () {
return function (vm, modal) {
return {
hi: function () {
// some code here, maybe it needs to reference the `vm` object, whatever...
}
}
};
})
The benefit of this approach is that you can test the MyHelperFactory on its own, without needing to instantiate YourController, and without needing to involve the $uibModal service. This is typically my favorite approach: no inline/anonymous functions - get that logic into helper factories, and out of my controllers.
Option 3: Drop the hi function onto vm
var app = angular.controller('YourController', function ($uibModal, MyHelperFactory) {
var vm = this;
// this pattern allows your function to be scoped with the not-yet-existing `modal` object
vm.hi = function (modal) {
return function () {
// some code here
}
};
vm.openPopup = function () {
$uibModal.open({
templateUrl: 'popup.html',
controller: function () {
var modal = this;
modal.hi = vm.hi(modal);
}
});
};
});
And from there, you can just test it by calling vm.hi from within your tests. I call this approach "dirty" because it adds the hi method to the vm object, and I generally avoid adding any properties to the vm object that aren't actually needed on the controller scope. In this case though, we're breaking that rule because it's the quickest/easiest way to "expose" this function that you wish to test.

Related

How to store controller functions in a service and call them in AngularJS

I need to execute functions of some controllers when my application ends (e.g. when closing the navigator tab) so I've thought in a service to manage the list of those functions and call them when needed. These functions changes depending on the controllers I have opened.
Here's some code
Controller 1
angular.module('myApp').component('myComponent', {
controller: function ($scope) {
var mc = this;
mc.saveData = function(objectToSave){
...
};
}
});
Controller 2
angular.module('myApp').component('anotherComponent', {
controller: function ($scope) {
var ac = this;
ac.printData = function(objects, priority){
...
};
}
});
How to store those functions (saveData & printData) considering they have different parameters, so when I need it, I can call them (myComponent.saveData & anotherComponent.printData).
The above code is not general controller but the angular1.5+ component with its own controller scope. So the methods saveData and printData can only be accessed in respective component HTML template.
So to utilise the above method anywhere in application, they should be part of some service\factory and that needs to be injected wherever you may required.
You can create service like :
angular.module('FTWApp').service('someService', function() {
this.saveData = function (objectToSave) {
// saveData method code
};
this.printData = function (objects, priority) {
// printData method code
};
});
and inject it wherever you need, like in your component:
controller: function(someService) {
// define method parameter data
someService.saveData(objectToSave);
someService.printData (objects, priority);
}
I managed to make this, creating a service for managing the methods that will be fired.
angular.module('FTWApp').service('myService',function(){
var ac = this;
ac.addMethodForOnClose = addMethodForOnClose;
ac.arrMethods = [];
function addMethodForOnClose(idModule, method){
ac.arrMethods[idModule] = {
id: idModule,
method: method
}
};
function executeMethodsOnClose(){
for(object in ac.arrayMethods){
ac.arrMethods[object].method();
}
});
Then in the controllers, just add the method needed to that array:
myService.addMethodForOnClose(id, vm.methodToLaunchOnClose);
Afterwards, capture the $window.onunload and run myService.executeMethodsOnClose()

Angular - How to show modal reject reason in table?

I have small problem to solve.
I have modal controller rejectIssueModalCtrl.js
(function () {
'use strict';
function rejectIssueModalCtrl($modalInstance, issue, $rootScope) {
var self = this;
self.cancel = function () {
$modalInstance.dismiss('cancel');
};
self.reject = function ($rootScope) {
$modalInstance.close(self.reason);
console.log(self.reason);
};
$rootScope.reasono = self.reason;
}
rejectIssueModalCtrl.$inject = ['$modalInstance', 'issue', '$rootScope'];
angular
.module('app')
.controller('rejectIssueModalCtrl', rejectIssueModalCtrl);
})();
When I click the button I can open this modal and write a reason. I want to show this reject reason in table in other controller.
Here's my code from other controller issueDetailsCtrl.js
$scope.reasonoo = $rootScope.reasono;
function rejectIssue() {
var rejectModal = $modal.open({
templateUrl: '/App/Issue/rejectIssueModal',
controller: 'rejectIssueModalCtrl',
controllerAs: 'rejectModal',
size: 'lg',
resolve: {
issue: self.issueData
}
});
rejectModal.result.then(function (reason) {
issueSvc
.rejectIssue(self.issueData.id, reason)
.then(function (issueStatus) {
changeIssueStatus(issueStatus.code);
getIssue();
}, requestFailed);
});
};
and html code
<div>
<span class="right" ng-bind="$root.reasono"></span>
</div>
As you can see I tried to use $rootScope. I can console.log the reason but I cant make it to show in this html. Any help?
We're missing some context, but I believe this is your problem:
self.reject = function ($rootScope) {
$modalInstance.close(self.reason);
console.log(self.reason);
};
$rootScope.reasono = self.reason;
Assuming self.reason is bound to the input in your modal, it won't be defined outside of the reject callback - that's the nature of async. You're able to log to console because you're doing so within the callback.
Define $rootScope.reasono inside of the callback like so:
self.reject = function () {
$modalInstance.close(self.reason);
console.log(self.reason);
$rootScope.reasono = self.reason;
};
Edited to show that $rootScope should be removed as a named parameter in the reject function definition.
Using root scope is not recommended. For this reason it is recommended to create a service for intercommuncation with variable to store reject reason, then inject this service for each controller - that way you will be able to read/write reason from different controllers.

Reusing angular mocks in Jasmine tests using $provide

I wish to reuse my mocks instead of having to set them up in every unit test that has them as dependency. But I'm having a hard time figuring out how to inject them properly.
Here's my attempt at unit test setup, which of course fails because ConfigServiceMockProvider doesn't exist.
describe('LoginService tests', function () {
var LoginService;
beforeEach(module('mocks'));
beforeEach(module('services.loginService', function ($provide, _ConfigServiceMock_) {
$provide.value("ConfigService", _ConfigServiceMock_);
/* instead of having to type e.g. everywhere ConfigService is used
* $provide.value("ConfigService", { 'foobar': function(){} });
*/
});
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
});
}
ConfigServiceMock
angular.module('mocks').service('ConfigServiceMock', function() {
this.init = function(){};
this.getValue = function(){};
}
I realize I probably could have ConfigServiceMock.js make a global window object, and thereby not needing to load it like this. But I feel there should be a better way.
Try something like this:
describe('Using externally defined mock', function() {
var ConfigServiceMock;
beforeEach(module('mocks'));
beforeEach(module('services.configService', function($provide) {
$provide.factory('ConfigService', function() {return ConfigServiceMock;});
}));
beforeEach(module('services.loginService'));
beforeEach(inject(function (_ConfigServiceMock_) {
ConfigServiceMock = _ConfigServiceMock_;
}));
// Do not combine this call with the one above
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
}));
it('should have been given the mock', function() {
expect(ConfigServiceMock).toBeDefined('The mock should have been defined');
expect(LoginService.injectedService).toBeDefined('Something should have been injected');
expect(LoginService.injectedService).toBe(ConfigServiceMock, 'The thing injected should be the mock');
});
});
According to this answer, you have to put all of your calls to module before all of your calls to inject.
This introduces a bit of a catch-22 because you have to have the reference to your ConfigServiceMock (via inject) into the spec before you can set it on the LoginService (done in the module call)
The work-around is to set an angular factory function as the ConfigService dependency. This will cause angular to lazy load the service, and by that time you will have received your reference to the ConfigServiceMock.

Testing complex Controller component

Below is the code for part of a controller I'm trying to unit test. I hate to post something that I have not yet attempted but I'm very lost as to how I should start testing this piece of code. I think part of the problem is I'm not sure how modalInstance is executed. If you can see a good entry point please let me know.
$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;
}
}
});
To start with, to make it properly testable, you might want to extract the nested controller to a first-class controller in the same angular module as this.
You can then test this current controller ( call it CtrlA) up to the point of confirmDelete and then test the newly extracted, so to say, 'modal controller' (lets call CtrlB) it separately.
In other words, in your tests for CtrlA, you ought to invoke confirmDelete and expect $modal.open toHaveBeenCalled. Note that in these tests, you would mock $modal.open:
spyOn(modal, 'open');
Under the hood, angular-ui-bootstrap instantiates a modal and sets up all the references correctly so that $modalInstance injected in CtrlB is same as modalInstance in your CtrlA that $modal.open returns. Since this is third party code, you need not 'test' the validity of this claim through your unit tests. You unit tests would simply assume that $modalInstance CtrlB has received is same as that created in CtrlA.
All that is left now is for you to test that each of the scope methods in CtrlB behave the way you them to. For that, you would create a spy object and inject it into the CtrlB as $modalInstance and you'd go about writing test as if you were writing tests for any ordinary controller.
For example, invoke cancel and test whether the spy $modalInstance.dismiss has been called (and so forth)
Best of luck!

bindToController in unit tests

I'm using bindToController in a directive to have the isolated scope directly attached to the controller, like this:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '#',
},
};
});
Then in the controller I have a default in case label is not specified in the HTML:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
How can I instantiate xxCtrl in the Jasmine unit tests so I can test the ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Check this to test the issue
In Angular 1.3 (see below for 1.4+)
Digging into the AngularJS source code I found an undocumented third argument to the $controller service called later (see $controller source).
If true, $controller() returns a Function with a property instance on which you can set properties.
When you're ready to instantiate the controller, call the function and it'll instantiate the controller with the properties available in the constructor.
Your example would work like this:
describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
Here's an updated Plunker (had to upgrade Angular to make it work, it's 1.3.0-rc.4 now): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
Note that it's probably not recommended to use it, to quote from the Angular source code:
Instantiate controller later: This machinery is used to create an
instance of the object before calling the controller's constructor
itself.
This allows properties to be added to the controller before the
constructor is invoked. Primarily, this is used for isolate scope
bindings in $compile.
This feature is not intended for use by applications, and is thus not
documented publicly.
However the lack of a mechanism to test controllers with bindToController: true made me use it nevertheless.. maybe the Angular guys should consider making that flag public.
Under the hood it uses a temporary constructor, we could also write it ourselves I guess.
The advantage to your solution is that the constructor isn't invoked twice, which could cause problems if the properties don't have default values as in your example.
Angular 1.4+ (Update 2015-12-06):
The Angular team has added direct support for this in version 1.4.0. (See #9425)
You can just pass an object to the $controller function:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
See also this blog post.
Unit Testing BindToController using ES6
If using ES6,you can import the controller directly and test without using angular mocks.
Directive:
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '#'
}
}
}
app.directive('xx', new xxDirective());
Controller:
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
Controller Test:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
see this for more information:
Proper unit testing of
Angular JS
applications with ES6 modules
In my view, this controller is not meant to be tested in isolation, because it will never work in isolation:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
It is tightly coupled with the directive relying on receiving its scope properties. It is not re-usable. From looking at this controller, I have to wonder where this variable is coming from. It is no better than a leaky function internally using a variable from outside scope:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
Now I have a leaky function whose behaviour is highly unpredictable based on the variable leakyVariable present (or not) in whatever scope the function is called.
Unsurprisingly this function is nightmare to test. Which is actually a good thing, perhaps to force the developer to rewrite the function into something more modular and re-usable. Which is not hard really:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
No leaky issues and really easy to test and re-use. Which to me tells that using the good old $scope is a better way:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
Simple, short and easy to test. Plus no bulky directive object definition.
The original reasoning behind the controllerAs syntax was the leaky scope inherited from the parent. However, directive's isolated scope already solves this problem. Thus I don't see any reason to use the bulkier leaky syntax.
I've found a way that is not particulary elegant but works at least (if there's a better option leave a comment).
We set the value that "comes" from the directive, and then we call the controller function again to test whatever it does. I've made a helper "invokeController" to be more DRY.
For example:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));

Resources