I'm writing a unit test for a controller that fires up a $modal
and uses the promise returned to execute some logic. I can test the parent controller that fires the $modal, but I can't for the life of me figure out how to mock a successful promise.
I've tried a number of ways, including using $q
and $scope.$apply()
to force the resolution of the promise. However, the closest I've gotten is putting together something similar to the last answer in this SO post;
I've seen this asked a few times with the "old" $dialog
modal.
I can't find much on how to do it with the "new" $dialog
modal.
Some pointers would be tres appreciated.
To illustrate the problem I'm using the example provided in the UI Bootstrap docs, with some minor edits.
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');
};
});
The 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>
The test
'use strict';
describe('Controller: MainCtrl', function() {
// load the controller's 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.
});
});
Brant's answer was clearly awesome, but this change made it even better for me:
then in the test area:
To add to Brant's answer, here is a slightly improved mock that will let you handle some other scenarios.
This will allow the mock to handle situations where...
You use the modal with the
.then()
,.catch()
and.finally()
handler style instead passing 2 functions (successCallback, errorCallback
) to a.then()
, for example:When you spy on the $modal.open function in the beforeEach,
you need to return a mock of what $modal.open normally returns, not a mock of $modal, which doesn’t include an
open
function as you laid out in yourfakeModal
mock. The fake modal must have aresult
object that contains athen
function to store the callbacks (to be called when the OK or Cancel buttons are clicked on). It also needs aclose
function (simulating an OK button click on the modal) and adismiss
function (simulating a Cancel button click on the modal). Theclose
anddismiss
functions call the necessary call back functions when called.Change the
fakeModal
to the following and the unit test will pass:Additionally, you can test the cancel dialog case by adding a property to test in the cancel handler, in this case
$scope.canceled
:Once the cancel flag is set, the unit test will look something like this:
Since modals use promises you should definitely use $q for such things.
Code becomes: