AngularJS Promise Callback Not Trigged in JasmineJS Test

14,702

Solution 1

TL;DR

Call $rootScope.$digest() from your test code and it'll pass:

it('should return false if user is not logged into Facebook', function () {
  ...

  var userLoggedIn;

  inject(function (Facebook, $rootScope) {
    Facebook.getUserLoginStatus($rootScope).then(function (data) {
      console.log("Found data!");
      userLoggedIn = data;
    });

    $rootScope.$digest(); // <-- This will resolve the promise created above
    expect(userLoggedIn).toEqual(false);
  });
});

Plunker here.

Note: I removed run() and wait() calls because they're not needed here (no actual async calls being performed).

Long explanation

Here's what's happening: When you call getUserLoginStatus(), it internally runs FB.getLoginStatus() which in turn executes its callback immediately, as it should, since you've mocked it to do precisely that. But your $scope.$apply() call is within that callback, so it gets executed before the .then() statement in the test. And since then() creates a new promise, a new digest is required for that promise to get resolved.

I believe this problem doesn't happen in the browser because of one out of two reasons:

  1. FB.getLoginStatus() doesn't invoke its callback immediately so any then() calls run first; or
  2. Something else in the application triggers a new digest cycle.

So, to wrap it up, if you create a promise within a test, explicitly or not, you'll have to trigger a digest cycle at some point in order for that promise to get resolved.

Solution 2

    'use strict';

describe('service: Facebook', function () {
    var rootScope, fb;
    beforeEach(module('app.services'));
    // Inject $rootScope here...
    beforeEach(inject(function($rootScope, Facebook){
        rootScope = $rootScope;
        fb = Facebook;
    }));

    // And run your apply here
    afterEach(function(){
        rootScope.$apply();
    });

    it('should return false if user is not logged into Facebook', function () {
        // Provide a fake version of the Facebook JavaScript SDK `FB` object:
        module(function ($provide) {
            $provide.value('fbsdk', {
                getLoginStatus: function (callback) { return callback({}); },
                init: function () {}
            });
        });
        fb.getUserLoginStatus($rootScope).then(function (data) {
            console.log("Found data!");
            expect(data).toBeFalsy(); // user is not logged in
        });
    });
}); // Service module spec

This should do what you're looking for. By using the beforeEach to set up your rootScope and afterEach to run the apply, you're also making your test easily extendable so you can add a test for if the user is logged in.

Share:
14,702
Admin
Author by

Admin

Updated on June 04, 2022

Comments

  • Admin
    Admin about 2 years

    Problem Intro

    I'm trying to unit test an AngularJS service that wraps the Facebook JavaScript SDK FB object; however, the test isn't working, and I haven't been able to figure out why. Also, the service code does work when I run it in a browser instead of a JasmineJS unit test, run with Karma test runner.

    I'm testing an asynchronous method using Angular promises via the $q object. I have the tests set up to run asynchronously using the Jasmine 1.3.1 async testing methods, but the waitsFor() function never returns true (see test code below), it just times-out after 5 seconds. (Karma doesn't ship with the Jasmine 2.0 async testing API yet).

    I think it might be because the then() method of the promise is never triggered (I've got a console.log() set up to show that), even though I'm calling $scope.$apply() when the asynchronous method returns, to let Angular know that it should run a digest cycle and trigger the then() callback...but I could be wrong.

    This is the error output that comes from running the test:

    Chrome 32.0.1700 (Mac OS X 10.9.1) service Facebook should return false
      if user is not logged into Facebook FAILED
      timeout: timed out after 5000 msec waiting for something to happen
    Chrome 32.0.1700 (Mac OS X 10.9.1):
      Executed 6 of 6 (1 FAILED) (5.722 secs / 5.574 secs)
    

    The Code

    This is my unit test for the service (see inline comments that explain what I've found so far):

    'use strict';
    
    describe('service', function () {
      beforeEach(module('app.services'));
    
      describe('Facebook', function () {
        it('should return false if user is not logged into Facebook', function () {
          // Provide a fake version of the Facebook JavaScript SDK `FB` object:
          module(function ($provide) {
            $provide.value('fbsdk', {
              getLoginStatus: function (callback) { return callback({}); },
              init: function () {}
            });
          });
    
          var done = false;
          var userLoggedIn = false;
    
          runs(function () {
            inject(function (Facebook, $rootScope) {
              Facebook.getUserLoginStatus($rootScope)
                // This `then()` callback never runs, even after I call
                // `$scope.$apply()` in the service :(
                .then(function (data) {
                  console.log("Found data!");
                  userLoggedIn = data;
                })
                .finally(function () {
                  console.log("Setting `done`...");
                  done = true;
                });
            });
          });
    
          // This just times-out after 5 seconds because `done` is never
          // updated to `true` in the `then()` method above :(
          waitsFor(function () {
            return done;
          });
    
          runs(function () {
            expect(userLoggedIn).toEqual(false);
          });
    
        }); // it()
      }); // Facebook spec
    }); // Service module spec
    

    And this is my Angular service that is being tested (see inline comments that explain what I've found so far):

    'use strict';
    
    angular.module('app.services', [])
      .value('fbsdk', window.FB)
      .factory('Facebook', ['fbsdk', '$q', function (FB, $q) {
    
        FB.init({
          appId: 'xxxxxxxxxxxxxxx',
          cookie: false,
          status: false,
          xfbml: false
        });
    
        function getUserLoginStatus ($scope) {
          var deferred = $q.defer();
          // This is where the deferred promise is resolved. Notice that I call
          // `$scope.$apply()` at the end to let Angular know to trigger the
          // `then()` callback in the caller of `getUserLoginStatus()`.
          FB.getLoginStatus(function (response) {
            if (response.authResponse) {
              deferred.resolve(true);
            } else {
              deferred.resolve(false)
            }
            $scope.$apply(); // <-- Tell Angular to trigger `then()`.
          });
    
          return deferred.promise;
        }
    
        return {
          getUserLoginStatus: getUserLoginStatus
        };
      }]);
    

    Resources

    Here is a list of other resources that I've already taken a look at to try to solve this problem.

    Summary

    Please note that I'm aware that there are already other Angular libraries out there for the Facebook JavaScript SDK, such as the following:

    I'm not interested in using them right now, because I wanted to learn how to write an Angular service myself. So please keep answers restricted to helping me fix the problems in my code, instead of suggesting that I use someone else's.

    So, with that being said, does anyone know why my test isn't working?

  • Admin
    Admin over 10 years
    With a little massaging, your solution above indeed work. However, I've accepted Michael's answer because he also explains why my test wasn't working in the first place. But thank you for your answer too!
  • Nick Perkins
    Nick Perkins almost 9 years
    I still don't understand it, but it works. Thank-you.
  • Chris Vandevelde
    Chris Vandevelde over 8 years
    I've been banging my head against the wall for three days about a related bug and you just nailed it. THANK YOU.
  • robert.bo.roth
    robert.bo.roth almost 8 years
    I'm having this same issue, and I've tried both $rootScope.$apply and $rootScope.$digest and it doesn't call my .then() either. Any other thoughts? In my case, it doesn't even call the .then() that's made directly after the $http() call that I've sent a json response back from my tests.