AngularJS Promise Callback Not Trigged in JasmineJS Test
angularjs, jasmine, javascript, karma-runner
Solution
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 any `then()` 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.
Problem
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. Angular API Reference: `$q` 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 the `then()` 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 - Answer 1 - Answer 2 - 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: - angular-easyfb - angular-facebook 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?