Mocking $ modal dans les tests unitaires AngularJS

J’écris un test unitaire pour un contrôleur qui déclenche un $modal et utilise la promesse retournée pour exécuter une logique. Je peux tester le contrôleur parent qui déclenche le $ modal, mais je ne peux pas pour la vie trouver comment réussir une promesse réussie.

J’ai essayé un certain nombre de façons, notamment en utilisant $q et $scope.$apply() pour forcer la résolution de la promesse. Cependant, le plus proche que j’ai eu est de rassembler quelque chose de similaire à la dernière réponse dans ce post SO;

J’ai vu ceci demandé à quelques resockets avec le “vieux” modal $dialog . Je ne trouve pas grand chose à faire avec le “nouveau” $dialog modal.

Certains conseils seraient très appréciés.

Pour illustrer le problème, j’utilise l’ exemple fourni dans la documentation de l’interface utilisateur Bootstrap , avec quelques modifications mineures.

Contrôleurs (principaux et modaux)

 'use ssortingct'; 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 vue (main.html)

 
Selection from a modal: {{ selected }}

Le test

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

    Lorsque vous espionnez la fonction $ modal.open dans beforeEach,

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

    vous devez retourner une maquette de ce que $ modal.open retourne normalement, pas une maquette de $ modal, qui n’inclut pas de fonction open comme vous l’avez exposé dans votre fakeModal maquette. Le faux modal doit avoir un object de result contenant une fonction pour stocker les rappels (à appeler lorsque les boutons OK ou Annuler sont cliqués). Il nécessite également une fonction de close (simulant un bouton OK, cliquez sur le modal) et une fonction de close (simulant un bouton Annuler, cliquez sur le modal). Les fonctions de close et de close appellent les fonctions de rappel nécessaires à l’appel.

    Remplacez le fakeModal par le suivant et le test unitaire réussira:

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

    De plus, vous pouvez tester la boîte de dialog d’annulation en ajoutant une propriété à tester dans le gestionnaire d’annulation, dans ce cas, $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()); }); 

    Une fois le drapeau d’annulation défini, le test unitaire ressemblera à ceci:

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

    Pour append à la réponse de Brant, voici une maquette légèrement améliorée qui vous permettra de gérer d’autres scénarios.

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

    Cela permettra à la simulation de gérer des situations où …

    Vous utilisez le modal avec le style de gestionnaire .then() , .catch() et .finally() , en passant 2 fonctions ( successCallback, errorCallback ) à un .then() , par exemple:

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

    Puisque les modaux utilisent des promesses, vous devriez certainement utiliser $ q pour de telles choses.

    Le code devient:

     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 réponse de Brant était clairement géniale, mais ce changement m’a rendu encore mieux:

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

    puis dans la zone de test:

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