Mocking $ modal en pruebas de unidad AngularJS

Estoy escribiendo una prueba unitaria para un controlador que dispara $modal y usa la promesa devuelta para ejecutar algo de lógica. Puedo probar el controlador principal que dispara $ modal, pero no puedo imaginar cómo burlarme de una promesa exitosa.

Lo intenté de varias maneras, incluido el uso de $q y $scope.$apply() para forzar la resolución de la promesa. Sin embargo, lo más cercano que he conseguido es armar algo similar a la última respuesta en este post SO;

Lo he visto algunas veces con el “viejo” modal de $dialog . No puedo encontrar mucho sobre cómo hacerlo con el “nuevo” modal modal de $dialog .

Algunos punteros serían muy apreciados.

Para ilustrar el problema, estoy usando el ejemplo proporcionado en los documentos de UI Bootstrap , con algunas ediciones menores.

Controladores (Principal y 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'); }; }); 

La vista (main.html)

 
Selection from a modal: {{ selected }}

La prueba

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

Cuando espias la función $ modal.open en beforeEach,

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

necesita devolver un simulacro de lo que $ modal.open normalmente devuelve, no un simulacro de $ modal, que no incluye una función open como la que presentó en su simulación de fakeModal . El modal falso debe tener un objeto de result que contenga una función para almacenar las devoluciones de llamada (para llamar cuando se hace clic en los botones Aceptar o Cancelar). También necesita una función de close (simulando un botón Aceptar, haga clic en el modal) y una función de close (simulando un botón Cancelar, haga clic en el modal). Las funciones de close y dismiss llaman a las funciones de callback necesarias cuando se las llama.

Cambie el fakeModal por el siguiente y pasará la prueba unitaria:

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

Además, puede probar el caso de diálogo cancelar agregando una propiedad para probar en el controlador de cancelación, en este caso $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()); }); 

Una vez que se establece el indicador de cancelación, la prueba de unidad se verá más o menos así:

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

Para agregar a la respuesta de Brant, aquí hay una simulación ligeramente mejorada que te permitirá manejar algunos otros escenarios.

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

Esto permitirá que el simulacro maneje situaciones donde …

Utiliza el modal con el estilo de controlador .then() , .catch() y .finally() lugar pasando 2 funciones ( successCallback, errorCallback ) a .then() , por ejemplo:

 modalInstance .result .then(function () { // close hander }) .catch(function () { // dismiss handler }) .finally(function () { // finally handler }); 

Como los modales usan promesas, definitivamente debes usar $ q para tales cosas.

El código se convierte en:

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

La respuesta de Brant fue claramente asombrosa, pero este cambio lo hizo aún mejor para mí:

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

luego en el área de prueba:

  finallyCallback() expect (thing finally callback does) .toEqual (what you would expect)