AngularJS Promise Callback Not Trigged in JasmineJS Test

前端 未结 3 623
挽巷
挽巷 2021-02-05 01:02

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 have

相关标签:
3条回答
  • 2021-02-05 01:07
        '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.

    0 讨论(0)
  • 2021-02-05 01:14

    From what I can see the problem of why your code isnt working is that you havent injected $scope. Michiels answer works cuz he injects $rootScope and call the digest cycle. However your $apply() is a higher level was of invoking the digest cycle so it will work as well... BUT! only if you inject it into the service itself.

    But i think a service doesn't create a $scope child so you need to inject $rootScope itself - as far as I know only controllers allow you to inject $scope as its their job to create well a $scope. But this is a bit of speculation, I am not 100 percent sure about it. I would try however with $rootScope as you know the app has a $rootScope from the creation of ng-app.

    'use strict';
    
    angular.module('app.services', [])
      .value('fbsdk', window.FB)
      .factory('Facebook', ['fbsdk', '$q', '$rootScope' function (FB, $q, $rootScope) { //<---No $rootScope injection
    
        //If you want to use a child scope instead then --> var $scope = $rootScope.$new();
        // Otherwise just use $rootScope
    
        FB.init({
          appId: 'xxxxxxxxxxxxxxx',
          cookie: false,
          status: false,
          xfbml: false
        });
    
        function getUserLoginStatus ($scope) { //<--- Use of scope here, but use $rootScope instead
          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()`. USE $rootScope instead!
          });
    
          return deferred.promise;
        }
    
        return {
          getUserLoginStatus: getUserLoginStatus
        };
      }]);
    
    0 讨论(0)
  • 2021-02-05 01:18

    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.

    0 讨论(0)
提交回复
热议问题