25

EDIT : Quick & Dirty solution at the end of this post

I am using a modal window from AngularUI-Bootstrap in the same way that it is explained on the website, except that I splitted files. Therefore I have :

CallingController.js :

$scope.delete = function () {
    if ($scope.selected.length > 0) {
        // [...]
        // preparing data
        // [...]
        var modalInstance = $modal.open({
            templateUrl: 'views/modalView.html',
            controller: 'modalCtrl',
            resolve: {
                itemArray: function () {
                    return $scope.selected;
                }
            }
        });
        modalInstance.result.then(function (confirm) {
            if (confirm === true) {
                // [...]
                // treat
                // [...]
            }
        });
    }
};

modalController.js :

myAppControllers.controller('modalCtrl',
    function ($scope, $modalInstance, itemArray) {

        $scope.accept = function () {
            $modalInstance.close(true);
        };

        $scope.reject = function () {
            $modalInstance.close(false);
        };

        $scope.itemArray = itemArray;

    });

and when I test this code with Karma (with the ui-bootstrap-tpls.min.js file loaded in the karma configuration file), I get the following error : Error: [$injector:unpr] [http://errors.angularjs.org/1.2.15-build.2389+sha.c5f2f58/$injector/unpr?p0=%24modalInstanceProvider%20%3C-%20%24modalInstance]1 at Error (native), meaning that jasmine doesn't manage to find the provider for $modalInstance.

I do not even test stuff on this controller, not yet, but here is my jasmine test file :

testModalController.js :

describe('Controller: modalCtrl', function () {

    beforeEach(module('myApp'));

    var Ctrl;
    var scope;

    // Initialize the controller and a mock scope
    beforeEach(inject(
        function ($controller, $rootScope) {
            scope = $rootScope.$new();

            Ctrl = $controller('modalCtrl', { $scope: scope });
        })
    );

    describe('Initial state', function () {
        it('should instantiate the controller properly', function () {
            expect(Ctrl).not.toBeUndefined();
        });

        it('should initialize its values properly', function () {

        });
    });

});

Have you got any clue about this problem ? It's not the first "external" module that I use (and test), and I did the same stuff than for the others, except that this time it doesn't work and I have no idea why.

==========================================

EDIT: Quick & probably dirty solution :

Okay, so based on the scope mocking method in the controller instantiation of Jasmine, I figured out how I could "solve" my problem, but it's probably quite dirty, so feel free to comment if you find a better way to do what I intend.

testModalController.js :

describe('Controller: modalCtrl', function () {

    beforeEach(module('myApp'));

    var Ctrl;
    var scope;
    var modalInstance;

    // Initialize the controller and a mock scope
    beforeEach(inject(
        function ($controller, $rootScope, _$modal_) {
            scope = $rootScope.$new();
            modalInstance = _$modal_.open({
                templateUrl: 'views/modalView.html'
            });

            Ctrl = $controller('modalCtrl', {
                $scope: scope,
                $modalInstance: modalInstance,
                itemArray: function () { return ['a', 'b', 'c']; }
            });
        })
    );

    describe('Initial state', function () {
        it('should instantiate the controller properly', function () {
            expect(Ctrl).not.toBeUndefined();
        });

        it('should initialize its values properly', function () {

        });
    });

});

This way, Jasmine doesn't search for providers anymore, because you already injected the items that are supposed to be needing those providers. It works, but I believe it could be done in a better way...

isherwood
  • 58,414
  • 16
  • 114
  • 157
Manyuuz
  • 278
  • 1
  • 3
  • 7
  • You need to load bootstrap module ('ui.bootstrap') or maybe ('ui.bootstrap.modal'). Try and tell me. Also a plunker always help. – Jesus Rodriguez Mar 07 '14 at 19:04
  • Same thing. Added dependencies, but same error keeps popping in console window. Any solution? What is triggering it? I tried [here](http://plnkr.co/edit/OuBIcvbe08FqPc6wjyBq?p=preview) and it is working fine, but one step to the left in my app and it is sending this hellish error in console. – Eugene Apr 07 '14 at 18:01

3 Answers3

65

I am solving this by just creating mock modal and modalInstance objects and verifying that they have been called by my controller code. Since modal and modalInstance are part of a third party library, it's not our responsibility to test that they work properly - rather, it's our responsibility to test that our code which calls the library is working ok.

Using your example:

describe('Controller: modalCtrl', function () {

  beforeEach(module('myApp'));

  var Ctrl;
  var scope;
  var modalInstance;

  // Initialize the controller and a mock scope
  beforeEach(inject(
    function ($controller, $rootScope) {     // Don't bother injecting a 'real' modal
      scope = $rootScope.$new();
      modalInstance = {                    // Create a mock object using spies
        close: jasmine.createSpy('modalInstance.close'),
        dismiss: jasmine.createSpy('modalInstance.dismiss'),
        result: {
          then: jasmine.createSpy('modalInstance.result.then')
        }
      };
      Ctrl = $controller('modalCtrl', {
        $scope: scope,
        $modalInstance: modalInstance,
        itemArray: function () { return ['a', 'b', 'c']; }
      });
    })
  );

  describe('Initial state', function () {
    it('should instantiate the controller properly', function () {
      expect(Ctrl).not.toBeUndefined();
    });

    it('should close the modal with result "true" when accepted', function () {
      scope.accept();
      expect(modalInstance.close).toHaveBeenCalledWith(true);
    });

    it('should close the modal with result "false" when rejected', function () {
      scope.reject();
      expect(modalInstance.close).toHaveBeenCalledWith(false);
    });
  });
});

This way, we don't really need any dependency on the Angular-UI objects and our unit tests are nice and isolated.

Tom Spencer
  • 7,816
  • 4
  • 54
  • 50
  • Thanks for this answer, I love it. I've been struggling for a while now trying to test my modals in a good way, and I think this handles it nicely! – Mik Cox May 13 '14 at 16:51
  • This is correct. It is important to add not to expect $modalInstance to be available to the injector in one's tests, as it would be to expect this of other services of the uiBootstrap module because $modalInstance is the instantiation of a new modal using the $modal service (which *is* available to the injector). – Morris Singer Sep 10 '14 at 23:46
7

Instead of:

modalInstance = {                    // Create a mock object using spies
  close: jasmine.createSpy('modalInstance.close'),
  dismiss: jasmine.createSpy('modalInstance.dismiss'),
  result: {
    then: jasmine.createSpy('modalInstance.result.then')
  }
};

This can be written as:

modalInstance = jasmine.createSpyObj('modalInstance', ['close', 'dismiss', 'result.then']);

Also there is no $modalInstance it is now $uibModalInstance so every "modalInstance" above should be replaced with "uibModalInstance"

Jason Rice
  • 446
  • 5
  • 8
  • +1 for the update about $uibModalInstance. Apparently this changed around the middle of October, 2015 when they moved from 0.14.3 to 1.0.0. – Scott Fraley Sep 08 '16 at 22:07
2

+1 for fiznool's answer. it is correct and should be chosen..

I would like to note one thing though, it is not maintainable the way it is presented here.

Since this is angular, I suggest use it..

angular.module('...').service('$modalInstance', function(){
   ... define spies and such 
})

would make your code much more modular and generic. simply add a file under spec somewhere with the above content and make sure to include it in your karma.conf

if you want to make sure it loads only in specific tests, simply give it a unique module name and add it to module invocation in beforeEach

guy mograbi
  • 27,391
  • 16
  • 83
  • 122