Mocking $ modal in AngularJS unit tests

I am writing a unit test for a controller that runs $modal and uses the promise returned to execute some logic. I can check the parent controller that runs $ modal, but I can’t understand for life how to mock a successful promise.

I tried several ways, including using $q and $scope.$apply() , to force the resolution of the promise. However, the closest I got is to put together something similar to the last answer in this in the SO post;

I have seen this several times with the "old" $dialog modal. I can’t learn much about how to do this with the β€œnew” $dialog modal.

Some pointers will be appreciated.

To illustrate the problem, I use the example provided in the UT Bootstrap docs with some minor changes.

Controllers (main and modal)

 'use strict'; angular.module('angularUiModalApp') .controller('MainCtrl', function($scope, $modal, $log) { $scope.items = ['item1', 'item2', 'item3']; $scope.open = function() { $scope.modalInstance = $modal.open({ templateUrl: 'myModalContent.html', controller: 'ModalInstanceCtrl', resolve: { items: function() { return $scope.items; } } }); $scope.modalInstance.result.then(function(selectedItem) { $scope.selected = selectedItem; }, function() { $log.info('Modal dismissed at: ' + new Date()); }); }; }) .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'); }; }); 

View (main.html)

 <div ng-controller="MainCtrl"> <script type="text/ng-template" id="myModalContent.html"> <div class="modal-header"> <h3>I is a modal!</h3> </div> <div class="modal-body"> <ul> <li ng-repeat="item in items"> <a ng-click="selected.item = item">{{ item }}</a> </li> </ul> Selected: <b>{{ selected.item }}</b> </div> <div class="modal-footer"> <button class="btn btn-primary" ng-click="ok()">OK</button> <button class="btn btn-warning" ng-click="cancel()">Cancel</button> </div> </script> <button class="btn btn-default" ng-click="open()">Open me!</button> <div ng-show="selected">Selection from a modal: {{ selected }}</div> </div> 

Test

 'use strict'; describe('Controller: MainCtrl', function() { // load the controller module beforeEach(module('angularUiModalApp')); var MainCtrl, scope; var fakeModal = { open: function() { return { result: { then: function(callback) { callback("item1"); } } }; } }; beforeEach(inject(function($modal) { spyOn($modal, 'open').andReturn(fakeModal); })); // Initialize the controller and a mock scope beforeEach(inject(function($controller, $rootScope, _$modal_) { scope = $rootScope.$new(); MainCtrl = $controller('MainCtrl', { $scope: scope, $modal: _$modal_ }); })); it('should show success when modal login returns success response', function() { expect(scope.items).toEqual(['item1', 'item2', 'item3']); // Mock out the modal closing, resolving with a selected item, say 1 scope.open(); // Open the modal scope.modalInstance.close('item1'); expect(scope.selected).toEqual('item1'); // No dice (scope.selected) is not defined according to Jasmine. }); }); 
+65
angularjs unit-testing angular-ui-bootstrap
Jan 19 '14 at 8:59
source share
4 answers

When you look at the $ modal.open function in beforeEach,

 spyOn($modal, 'open').andReturn(fakeModal); or spyOn($modal, 'open').and.returnValue(fakeModal); //For Jasmine 2.0+ 

you need to return the layout of what $ modal.open usually returns, and not the layout of $ modal, which does not include the open function, as you laid out in the fakeModal layout. A fake modal must have a result object that contains the then function for storing callbacks (called when the "OK" or "Cancel" buttons are clicked). He also needs the close function (simulating the OK button that clicks on the modal) and the dismiss function (simulating the Cancel button on the modal). The close and dismiss functions call the necessary callback functions when called.

Change fakeModal to the following and unit test will pass:

 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 ); } }; 

Alternatively, you can test the undo dialog by adding a property to check in the undo handler, in this case $scope.canceled :

 $scope.modalInstance.result.then(function (selectedItem) { $scope.selected = selectedItem; }, function () { $scope.canceled = true; //Mark the modal as canceled $log.info('Modal dismissed at: ' + new Date()); }); 

Once the cancel flag is set, the unit test will look something like this:

 it("should cancel the dialog when dismiss is called, and $scope.canceled should be true", function () { expect( scope.canceled ).toBeUndefined(); scope.open(); // Open the modal scope.modalInstance.dismiss( "cancel" ); //Call dismiss (simulating clicking the cancel button on the modal) expect( scope.canceled ).toBe( true ); }); 
+90
Jan 26 '14 at 23:31
source share

To add to Brant's answer, here is a slightly improved layout that will allow you to handle some other scenarios.

 var fakeModal = { result: { then: function (confirmCallback, cancelCallback) { this.confirmCallBack = confirmCallback; this.cancelCallback = cancelCallback; return this; }, catch: function (cancelCallback) { this.cancelCallback = cancelCallback; return this; }, finally: function (finallyCallback) { this.finallyCallback = finallyCallback; return this; } }, close: function (item) { this.result.confirmCallBack(item); }, dismiss: function (item) { this.result.cancelCallback(item); }, finally: function () { this.result.finallyCallback(); } }; 

This will allow the layout to handle situations in which ...

You use the modal style .then() , .catch() and .finally() instead of two functions ( successCallback, errorCallback ) for .then() , for example:

 modalInstance .result .then(function () { // close hander }) .catch(function () { // dismiss handler }) .finally(function () { // finally handler }); 
+9
Dec 15 '14 at 3:01
source share

Since modals use promises, you should definitely use $ q for such things.

The code becomes:

 function FakeModal(){ this.resultDeferred = $q.defer(); this.result = this.resultDeferred.promise; } FakeModal.prototype.open = function(options){ return this; }; FakeModal.prototype.close = function (item) { this.resultDeferred.resolve(item); $rootScope.$apply(); // Propagate promise resolution to 'then' functions using $apply(). }; FakeModal.prototype.dismiss = function (item) { this.resultDeferred.reject(item); $rootScope.$apply(); // Propagate promise resolution to 'then' functions using $apply(). }; // .... // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); fakeModal = new FakeModal(); MainCtrl = $controller('MainCtrl', { $scope: scope, $modal: fakeModal }); })); // .... it("should cancel the dialog when dismiss is called, and $scope.canceled should be true", function () { expect( scope.canceled ).toBeUndefined(); fakeModal.dismiss( "cancel" ); //Call dismiss (simulating clicking the cancel button on the modal) expect( scope.canceled ).toBe( true ); }); 
+3
Aug 22 '15 at 13:26
source share

Brant's answer was clearly terrific, but this change made it even better for me:

  fakeModal = opened: then: (openedCallback) -> openedCallback() result: finally: (callback) -> finallyCallback = callback 

then in the test area:

  finallyCallback() expect (thing finally callback does) .toEqual (what you would expect) 
+2
Oct 03 '14 at 21:37
source share



All Articles