AngularJS Promise Callback Not Trigged in JasmineJS Test
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:
-
FB.getLoginStatus()
doesn't invoke its callback immediately so anythen()
calls run first; or - 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.
![Admin](/assets/logo_square_200-5d0d61d6853298bd2a4fe063103715b4daf2819fc21225efa21dfb93e61952ea.png)
Admin
Updated on June 04, 2022Comments
-
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 thewaitsFor()
function never returnstrue
(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 aconsole.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 thethen()
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.
-
This explains how to use promises in Angular, as well as giving an example of how to unit-test code that uses promises (note the explanation of why
$scope.$apply()
needs to be called to trigger thethen()
callback). -
Jasmine Async Testing Examples
- Jasmine.Async: Making Asynchronous Testing With Jasmine Suck Less
-
Testing Asynchronous Javascript w/ Jasmine 2.0.0
These give examples of how to use the Jasmine 1.3.1 async methods to test objects implementing the Promise pattern. They're slightly different from the pattern I used in my own test, which is modeled after the example that comes directly from the Jasmine 1.3.1 async testing documentation.
-
StackOverflow Answers
- Promise callback not called in Angular JS
- angularjs - promise never resolved in controller
- AngularJS promises not firing when returned from a service
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 over 10 yearsWith 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 almost 9 yearsI still don't understand it, but it works. Thank-you.
-
Chris Vandevelde over 8 yearsI'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 almost 8 yearsI'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.