I have created a common ModalService and this is used for two diferrnt type of dialogs. CancelDialog and ErrorDialog will be popped up as per parameter passed to service.
Why do we Unit Test when functionality is working fine??
i.e This will show an ErrorDialog
ModalService.openModal('Analysis Error', 'I am Error Type', 'Error');
All is working fine but am stuck with Unit Test. Here is working PLUNKER.
Please help in covering Unit Test for this.
How to do Unit Test for openErrorModal & openCancelModal in below service.
ModalService
// common modal service
validationApp.service('ModalService',
function($uibModal) {
return {
openModal: openModal
};
function openErrorModal(title, message, callback) {
$uibModal.open({
templateUrl: 'ErrorDialog.html',
controller: 'ErrorDialogCtrl',
controllerAs: 'vm',
backdrop: 'static',
size: 'md',
resolve: {
message: function() {
return message;
},
title: function() {
return title;
},
callback: function() {
return callback;
}
}
});
}
function openCancelModal(title, message, callback) {
$uibModal.open({
templateUrl: 'CancelDialog.html',
controller: 'ErrorDialogCtrl',
controllerAs: 'vm',
backdrop: 'static',
size: 'md',
resolve: {
message: function() {
return message;
},
title: function() {
return title;
},
callback: function() {
return callback;
}
}
});
}
function openModal(title, message, modalType, callback) {
if (modalType === "Error") {
openErrorModal(title, message, callback);
} else {
openCancelModal(title, message, callback);
}
}
}
);
How to Unit Test onOk , onContinue & onDiscard in below controller.
DialogController
//controller fot dialog
validationApp.controller('ErrorDialogCtrl',
function($uibModalInstance, message, title, callback) {
alert('from controller');
var vm = this;
vm.message = message;
vm.onOk = onOk;
vm.onContinue = onContinue;
vm.onDiscard = onDiscard;
vm.callback = callback;
vm.title = title;
function onOk() {
$uibModalInstance.close();
}
function onContinue() {
$uibModalInstance.close();
}
function onDiscard() {
vm.callback();
$uibModalInstance.close();
}
});
You need to separately test service and controllers. For controllers, you need to test that methods of uibModalInstance are called when controller methods are called. You don't actually need to test that dialog closes, when close method is called. That is the task of those who implemented uibModal.
So here is the test for controller:
describe('ErrorDialogCtrl', function() {
// inject the module of your controller
beforeEach(module('app'));
var $controller;
beforeEach(inject(function(_$controller_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$controller = _$controller_;
}));
it('tests that close method is called on modal dialog', function() {
var $uibModalInstance = {
close: jasmine.createSpy('close')
};
var callback = function() {};
var controller = $controller('PasswordController', { $uibModalInstance: $uibModalInstance, message: {}, callback: callback });
controller.onOk();
expect($uibModalInstance.close).toHaveBeenCalled();
});
});
Here is the simply test for service:
describe('ModalService', function () {
var $injector;
var $uibModal;
// inject the module of your controller
beforeEach(module('app', function($provide) {
$uibModal = {
open: jasmine.createSpy('open')
};
$provide.value('$uibModal', $uibModal);
}));
beforeEach(inject(function (_$injector_) {
$injector = _$injector_;
}));
it('tests that openErrorModal is called', function () {
var modalService = $injector.get('ModalService');
modalService.openModal(null, null, "Error");
expect($uibModal.open).toHaveBeenCalledWith(jasmine.objectContaining({
controller: "ErrorDialogCtrl"
}));
});
});
Related
I'm working on some legacy code that uses angularjs 1.x for a web frontend. I need to create a modal dialog that will make a RESTful call to the backend when the modal is opened and wait for the data to be returned before rendering the view.
I was able to figure out most of what I needed to do, but there is one thing I still can't wrap my head around. My understanding was that I needed to use 'resolve' to define a function that would return a $promise to the controller. When I put a breakpoint inside my controller though, the parameter is an object containing the promise, the resolution status, and finally my actual data.
I can pull the data I need out of this object, but it feels like I shouldn't have to do that. My controller doesn't care about the promise itself; just the data that got returned. Is there some way to structure this so only the data gets sent to the controller or is this just how angular modals are expected to behave?
A sample of my code:
$scope.openTerritorySelect = function () {
var modalInstance = $modal.open({
animation: true,
templateUrl: 'prospect/detail/selectTerritoriesModal.tpl.html',
controller: function($scope, $modalInstance, availableReps){
$scope.reps = availableReps;
$scope.ok=function()
{
$modalInstance.close();
};
$scope.cancel=function()
{
$modalInstance.dismiss('cancel');
};
},
resolve: {
availableReps: function () {
return Prospect.getRelatedReps({}, function (data, header) {
$scope.busy = false;
return data.result;
}, function (response) {
$scope.busy = false;
if (response.status === 404) {
$rootScope.navError = "Could not get reps";
$location.path("/naverror");
}
}).$promise;
}
}
});
modalInstance.result.then(function (selectedReps) {
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
};
The 'Prospect' service class:
angular.module('customer.prospect', [ "ngResource" ]).factory('Prospect', [ 'contextRoute', '$resource', function(contextRoute, $resource) {
return {
getRelatedReps : function(args, success, fail) {
return this.payload.getRelatedReps(args, success, fail);
},
payload : $resource(contextRoute + '/api/v1/prospects/:id', {
}, {
'getRelatedReps' : {
url : contextRoute + '/api/v1/prospects/territories/reps',
method : 'GET',
isArray : false
}
})
};
} ]);
You could simplify things a great deal by making the REST request before you even open the modal. Would you even want to open the modal if the request were to fail?
$scope.openTerritorySelect = function () {
Prospect.getRelatedReps({}, function (data, header) {
$scope.busy = false;
var modalInstance = $modal.open({
animation: true,
templateUrl: 'prospect/detail/selectTerritoriesModal.tpl.html',
controller: function($scope, $modalInstance, availableReps){
$scope.reps = availableReps;
$scope.ok = function() {
$modalInstance.close();
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
},
resolve: {
availableReps: function () {
return data.result;
}
});
modalInstance.result.then(function (selectedReps) {},
function () {
console.log('Modal dismissed at: ' + new Date());
});
}, function (response) {
$scope.busy = false;
if (response.status === 404) {
$rootScope.navError = "Could not get reps";
$location.path("/naverror");
}
});
};
I have a factory for create dialog:
module myModule {
export class myDialog {
constructor(private $uibModal: ng.ui.bootstrap.IModalService) {}
showDialog() {
var options: ng.ui.bootstrap.IModalSettings = {
templateUrl: '/dialog.html',
size: "lg",
controller: ['$scope', '$uibModalInstance', function($scope: any, $uibModalInstance: ng.ui.bootstrap.IModalServiceInstance) {
$scope.cancel = () => {
$uibModalInstance.close({
doAction: 'close'
});
}
}],
controllerAs: '$ctrl'
};
return this.$uibModal.open(options).result;
}
static factory(): any {
const dialog = ($uibModal: ng.ui.bootstrap.IModalService) => new myDialog($uibModal);
dialog.$inject = ['$uibModal'];
return dialog;
}
};
angular.module('myModule').factory('myDialog', myDialog.factory());
}
As you see, for controller injection I used array (inline array annotation) in order to work when javascript file is minified.
I created a test using bardjs:
describe('My dialog service', function() {
beforeEach(function() {
module('myModule', function($provide) {
$provide.factory('$uibModalInstance', function() {
return {
close: function(result) {
return result;
}
};
});
});
module('myModule');
bard.inject('$uibModal', '$uibModalInstance', '$http', '$httpBackend', '$q', '$rootScope', 'myDialog');
bard.mockService($uibModal, {
open: function(options) {
return {
result: options
};
}
});
spyOn($uibModal, 'open').and.callThrough();
spyOn($uibModalInstance, 'close').and.callThrough();
});
it('expect it to be defined', function() {
expect(myDialog).toBeDefined();
});
});
But I got error:
TypeError: Array is not a constructor (evaluating
'options.controller(scope,$uibModalInstance)')
Why ? How to solve it ?
Declare controller outside options object and use $inject to inject dependencies.
showDialog() {
const ctrl = function($scope: any, $uibModalInstance: ng.ui.bootstrap.IModalServiceInstance) {
$scope.cancel = () => {
$uibModalInstance.close({
doAction: 'close'
});
}
};
ctrl.$inject = ['$scope', '$uibModalInstance'];
var options: ng.ui.bootstrap.IModalSettings = {
templateUrl: '/dialog.html',
size: "lg",
controller: ctrl,
controllerAs: '$ctrl'
};
return this.$uibModal.open(options).result;
}
My angular app has a controller that can be used both in modal mode and in no modal mode. I would like to check in which mode it is. Can someone help me ?
Order controller
$scope.chooseClient = function() {
$uibModal.open({
templateUrl: 'partials/client/edit.html',
controller: 'ClientEditController',
}).result.then(function (client) {
// Modal OK
if (client) {
$scope.model.client = client;
}
}, function (status) {
// Modal cancelado
});
};
Client controller
.controller('ClientEditController',
function ($scope, $location) {
$scope.cancel = function() {
if (//I would check if modal mode) {
$scope.$dismiss('cancel');
} else {
$location.path("/client/list");
}
};
});
You can pass a resolve object with a flag. That will force you to pass the same resolve in your route for the controller, and inject to resolve to the controller.
$scope.chooseClient = function() {
$uibModal.open({
templateUrl: 'partials/client/edit.html',
controller: 'ClientEditController',
resolve: {
isModal: true
}
}).result.then(function (client) {
// Modal OK
if (client) {
$scope.model.client = client;
}
}, function (status) {
// Modal cancelado
});
};
And than check:
.controller('ClientEditController', function ($scope, $location, isModal) {
$scope.cancel = function() {
if (isModal) {
$scope.$dismiss('cancel');
} else {
$location.path("/client/list");
}
};
});
See docs: $uibModal
here is the code:
(function(){
"use strict";
angular.module("dataModule")
.controller("panelController", ["$scope", "$state", "$timeout", "$modal", panelController]);
function panelController($scope, $state, $timeout, $modal){
$scope.property = "panelController";
//how do we do unit test on openCancelWarning.
//i did not find a way to get openCancelWarning function in Jasmine.
function openCancelWarning () {
var cancelModal = $modal.open({
animation: true,
backdrop: "static",
templateUrl: "pages/data/cancel-warning.html",
controller: "cancelWarningController",
size: "sm",
resolve: {
items : function() {
return {
warningTitle : "Are you Sure?",
warningMessage: "There are unsaved changes on this page. are you sure you want to navigate away from this page?Click OK to continue or Cancel to stay on this page"
};
}
}
});
return cancelModal;
}
var resultPromise = openCancelWarning();
var result;
resultPromise.result.then(function(response){
result = response;
});
}
angular.module("dataModule")
.controller("cancelWarningController", ["$scope", "$modalInstance", "items", cancelWarningController]);
function cancelWarningController($scope, $modalInstance, items){
$scope.warningTitle = items.warningTitle;
$scope.warningMessage = items.warningMessage;
$scope.cancel = function() {
$modalInstance.close(false);
};
$scope.ok = function() {
$modalInstance.close(true);
};
}
}());
here is my jasmine unit test code.
describe("Controller: panelController", function () {
beforeEach(module("dataModule"));
var panelController, scope;
var fakeModal = {
result : {
then: function(confirmCallback) {
this.confirmCallback = confirmCallback;
}
},
close: function(confirmResult) {
this.result.confirmCallback(confirmResult);
}
};
beforeEach(inject(function($modal) {
spyOn($modal, "open").andReturn(fakeModal);
}));
beforeEach(inject(function ($controller, $rootScope, _$modal_) {
scope = $rootScope.$new();
panelController = $controller("panelController", {
$scope: scope,
$modal: _$modal_
});
}));
it('test should be true', function () {
var test;
var testResult = panelController.openCancelWarning();
testResult.close(true);
testResult.then(function(response){
test=response;
});
expect(test).toBe(true);
});
});
i wrote above unit test code with the help from Mocking $modal in AngularJS unit tests
i always get below error.
TypeError: 'undefined' is not a function (evaluating 'panelController.openCancelWarning()')
could anyone help this?
I'm having problems trying to write a jasmine unit test for an Angular-Bootstrap $modal. The exact error is
Expected spy open to have been called with [ { templateUrl : '/n/views/consent.html', controller : 'W2ConsentModal as w2modal', resolve : { employee : Function }, size : 'lg' } ] but actual calls were [ { templateUrl : '/n/views/consent.html', controller : 'W2ConsentModal as w2modal', resolve : { employee : Function }, size : 'lg' } ]
The expected and actual modal options object are the same. What is going on?
Controller
(function () {
'use strict';
angular
.module('app')
.controller('W2History', W2History);
W2History.$inject = ['$scope', '$modal', 'w2Service'];
function W2History($scope, $modal, w2Service) {
/* jshint validthis:true */
var vm = this;
vm.showModal = showModal;
function showModal(employee) {
var modalInstance = $modal.open({
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
});
modalInstance.result.then(function (didConsent) {
// code omitted
});
}
}
})();
Test
describe('W2History controller', function () {
var controller, scope, modal;
var fakeModal = {
result: {
then: function (confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function (item) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.result.confirmCallBack(item);
},
dismiss: function (type) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.result.cancelCallback(type);
}
};
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
};
beforeEach(function () {
module('app');
inject(function (_$controller_, _$rootScope_, _$modal_) {
scope = _$rootScope_.$new();
modal = _$modal_;
spyOn(modal, 'open').and.returnValue(fakeModal);
controller = _$controller_('W2History', {
$scope: scope,
$modal: modal,
w2Service: w2Srvc
});
});
});
it('Should correctly show the W2 consent modal', function () {
var employee = terminatedaccessMocks.getCurrentUserInfo();
controller.showModal(employee);
expect(modal.open).toHaveBeenCalledWith(modalOptions);
});
});
Try this:
describe('W2History controller', function () {
var controller, scope, modal;
var fakeModal = {
result: {
then: function (confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function (item) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.result.confirmCallBack(item);
},
dismiss: function (type) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.result.cancelCallback(type);
}
};
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: jasmine.any(Function)
},
size: 'lg'
};
var actualOptions;
beforeEach(function () {
module('plunker');
inject(function (_$controller_, _$rootScope_, _$modal_) {
scope = _$rootScope_.$new();
modal = _$modal_;
spyOn(modal, 'open').and.callFake(function(options){
actualOptions = options;
return fakeModal;
});
controller = _$controller_('W2History', {
$scope: scope,
$modal: modal
});
});
});
it('Should correctly show the W2 consent modal', function () {
var employee = { name : "test"};
controller.showModal(employee);
expect(modal.open).toHaveBeenCalledWith(modalOptions);
expect(actualOptions.resolve.employee()).toEqual(employee);
});
});
PLUNK
Explanation:
We should not expect the actual resolve.employee to be the same with the fake resolve.employee because resolve.employee is a function which returns an employee (in this case the employee is captured in closure). The function could be the same but at runtime the returned objects could be different.
The reason your test is failing is the way javascript compares functions. Take a look at this fiddle. Anyway, I don't care about this because we should not expect function implementations. What we do care about in this case is the resolve.employee returns the same object as we pass in:
expect(actualOptions.resolve.employee()).toEqual(employee);
So the solution here is:
We expect everything except for the resolve.employee:
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: jasmine.any(Function) //don't care about the function as we check it separately.
},
size: 'lg'
};
expect(modal.open).toHaveBeenCalledWith(modalOptions);
Check the resolve.employee separately by capturing it first:
var actualOptions;
spyOn(modal, 'open').and.callFake(function(options){
actualOptions = options; //capture the actual options
return fakeModal;
});
expect(actualOptions.resolve.employee()).toEqual(employee); //Check the returned employee is actually the one we pass in.
This is a pass by reference vs pass by value issue. The resolve.employee anonymous function used in $modal.open:
var modalInstance = $modal.open({
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
});
is not the same (by reference) as the resolve.employee anonymous function in your test:
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
};
Your test should be:
resolve: {
employee: jasmine.any(Function)
}
If it's essential that the resolve function be tested, you should expose it somewhere where you can get a reference to the same function in your tests.
I am not sure if this will help you now, but when you spy on something you can get the argument that is passed to the $uibModal.open spy, you can then call that function to test that it returns what is in the resolve method.
it('expect resolve to be have metadataid that will return 9999', () => {
spyOn($uibModal, 'open');
//add test code here that will call the $uibModal.open
var spy = <jasmine.Spy>$uibModal.open;
var args = spy.calls.argsFor(0);
expect(args[0].resolve.metadataId()).toEqual(9999);
});
***** my code is using typescript, but this works for me.**
I have come across the same scenario. I have come across the problem with the below given solution
//Function to open export modal
scope.openExportModal();
expect( uibModal.open ).toHaveBeenCalledWith(options);
expect( uibModal.open.calls.mostRecent().args[0].resolve.modalData() ).toEqual(modalData);
Hope this may help if you want a quick fix.