3

THE SITUATION:

I am unit testing my Angular / Ionic app.

I am having troubles with the modal. At the moment i can test that the modal has been called. That's all so far. I cannot test the proper show() and hide() method of the modal.

I am getting the following errors:

TypeError: $scope.modal_login.show is not a function
Error: show() method does not exist

TypeError: $scope.modal_login.hide is not a function
Error: hide() method does not exist

I think it depends entirely on the spy. I don't know how to properly spy on the modal, and i think that once that is done, everything will work fine.

THE CODE:

The controller:

$scope.open_login_modal = function() 
{
    var temp = $ionicModal.fromTemplateUrl('templates/login.html',{scope: $scope});

    temp.then(function(modal) { 
        $scope.modal_login = modal;
        $scope.modal_login.show();

        $scope.for_test_only = true;
    });
};

$scope.close_login_modal = function() 
{
    $scope.modal_login.hide();
};

Note: the code of open_login_modal function has been refactored to facilitate the test. The original code was:

$scope.open_login_modal = function() 
{
    $ionicModal.fromTemplateUrl('templates/login.html', {
        scope: $scope
    }).then(function(modal) {

        $scope.modal_login = modal;
        $scope.modal_login.show();
    }); 
};

The test:

describe('App tests', function() 
{
    beforeEach(module('my_app.controllers'));

    function fakeTemplate() 
    {
        return { 
            then: function(modal){
                $scope.modal_login = modal;
            }
        }
    }

    beforeEach(inject(function(_$controller_, _$rootScope_)
    {
        $controller = _$controller_;
        $rootScope = _$rootScope_;
        $scope = _$rootScope_.$new();

        $ionicModal = 
        {
            fromTemplateUrl: jasmine.createSpy('$ionicModal.fromTemplateUrl').and.callFake(fakeTemplate)
        }; 

        var controller = $controller('MainCtrl', { $scope: $scope, $rootScope: $rootScope, $ionicModal: $ionicModal });
    }));


    describe('Modal tests', function() 
    {
        beforeEach(function()
        {
            $scope.open_login_modal();
            spyOn($scope.modal_login, 'show'); // NOT WORKING
            spyOn($scope.modal_login, 'hide'); // NOT WORKING
        });

        it('should open login modal', function() 
        {
            expect($ionicModal.fromTemplateUrl).toHaveBeenCalled(); // OK
            expect($ionicModal.fromTemplateUrl.calls.count()).toBe(1); // OK
            expect($scope.modal_login.show()).toHaveBeenCalled(); // NOT PASS
            expect($scope.for_test_only).toEqual(true); // NOT PASS
        });

        it('should close login modal', function() 
        {
            $scope.close_login_modal();     
            expect($scope.modal_login.hide()).toHaveBeenCalled(); // NOT PASS
        });
    });

});

As you can see from the code $scope.for_test_only it should be equal to true but is not recognized. I get this error message instead:

Expected undefined to equal true.

The same happens to the show() and hide() method. They are not seen by the test.

And i think because they are not declared in the spy.

THE QUESTION:

How can i properly spy on a modal?

Thank you very much!

FrancescoMussi
  • 20,760
  • 39
  • 126
  • 178

1 Answers1

3

The question here could be extrapolated to how to properly spy on a promise. You are very much on the right track here.

However, if you want to test that whatever your callback to the success of the promise is called, you have to execute two steps:

  1. Mock the service (in your case $ionicModal) and return some fake function
  2. In that fake function, execute the callback that is passed to you by the production code.

Here is an illustration:

//create a mock of the service (step 1)
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);

//create an example response which just calls your callback (step2)
var successCallback = {
   then: function(callback){
       callback.apply(arguments);
   }
};

$ionicModal.fromTemplateUrl.and.returnValue(successCallback);

Of course, you can always use $q if you don't want to be maintaining the promise on your own:

//in your beforeeach
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);
//create a mock of the modal you gonna pass and resolve at your fake resolve
var modalMock = jasmine.createSpyObj('modal', ['show', 'hide']);
$ionicModal.fromTemplateUrl.and.callFake(function(){
    return $q.when(modalMock);
});


//in your test
//call scope $digest to trigger the angular digest/apply lifecycle
$scope.$digest();
//expect stuff to happen
expect(modalMock.show).toHaveBeenCalled();
Nikola Yovchev
  • 9,498
  • 4
  • 46
  • 72
  • Oh my God! Thank you! Has been so much time crashing the head against the wall! I am ready to build a statue in your honour @baba! Also very good reply giving the general rule. Also is interesting that $scope.$digest(); was essential to make everything working. I will study more why it is like that. – FrancescoMussi Oct 27 '15 at 13:13
  • 2
    haha, no problem, I have to say I also spent some grueling moments trying to figure that out but I am always glad to help. – Nikola Yovchev Oct 27 '15 at 14:13