web-dev-qa-db-fra.com

AngularJS Promise Callback non déclenché dans le test Jasmine JS

Introduction au problème

J'essaie de tester un service AngularJS qui encapsule l'objet Facebook JavaScript SDKFB; cependant, le test ne fonctionne pas et je n'ai pas pu comprendre pourquoi. En outre, le code de service fonctionne lorsque je l'exécute dans un navigateur au lieu d'un JasmineJS test unitaire, exécutez avec Karma test runner .

Je teste une méthode asynchrone en utilisant Angular promesses via l'objet $q . J'ai les tests configurés pour fonctionner de manière asynchrone en utilisant le Méthodes de test asynchrone Jasmine 1.3.1 , mais la fonction waitsFor() ne retourne jamais true (voir le code de test ci-dessous), il suffit de temps- après 5 secondes (Karma n'est pas encore livré avec l'API de test asynchrone Jasmine 2.0).

Je pense que c'est peut-être parce que la méthode then() de la promesse n'est jamais déclenchée (j'ai une console.log() configurée pour le montrer), même si j'appelle $scope.$apply() lorsque la méthode asynchrone revient, pour laisser Angular savoir qu'elle doit exécuter un cycle de résumé et déclencher le rappel then() ... mais je peux me tromper.

Il s'agit de la sortie d'erreur résultant de l'exécution du 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)

Le code

Ceci est mon test unitaire pour le service ( voir les commentaires en ligne qui expliquent ce que j'ai trouvé jusqu'à présent ):

'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

Et ceci est mon Angular qui est testé ( voir les commentaires en ligne qui expliquent ce que j'ai trouvé jusqu'à présent ):

'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
    };
  }]);

Ressources

Voici une liste d'autres ressources que j'ai déjà consultées pour essayer de résoudre ce problème.

Sommaire

Veuillez noter que je suis conscient qu'il existe déjà d'autres bibliothèques Angular pour le SDK JavaScript Facebook , telles que comme suit:

Je ne suis pas intéressé à les utiliser pour le moment, car je voulais apprendre à écrire moi-même un service Angular. Veuillez donc limiter les réponses à l'aide résoudre les problèmes dans mon code, au lieu de suggérer que j'utilise celui de quelqu'un d'autre .

Donc, cela étant dit, quelqu'un sait-il pourquoi mon test ne fonctionne pas?

40
user456814

TL; DR

Appelez $rootScope.$digest() à partir de votre code de test et cela passera:

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 ici .

Remarque: J'ai supprimé les appels run() et wait() car ils ne sont pas nécessaires ici (aucun appel asynchrone réel n'est effectué).

Explication longue

Voici ce qui se passe: lorsque vous appelez getUserLoginStatus(), il exécute en interne FB.getLoginStatus() qui à son tour exécute son rappel immédiatement , comme il se doit, puisque vous vous êtes moqué de faire exactement cela. Mais votre appel $scope.$apply() est dans ce rappel, donc il est exécuté avant l'instruction .then() dans le test. Et puisque then() crée une nouvelle promesse, un nouveau résumé est requis pour que cette promesse soit résolue.

Je crois que ce problème ne se produit pas dans le navigateur pour une raison sur deux:

  1. FB.getLoginStatus() n'invoque pas son rappel immédiatement, donc tout appel then() s'exécute en premier; ou
  2. Quelque chose d'autre dans l'application déclenche un nouveau cycle de résumé.

Donc, pour conclure, si vous créez une promesse dans un test, explicitement ou non, vous devrez déclencher un cycle de résumé à un moment donné pour que cette promesse soit résolue.

52
Michael Benford
    '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

Cela devrait faire ce que vous cherchez. En utilisant beforeEach pour configurer votre rootScope et afterEach pour exécuter l'application, vous rendez également votre test facilement extensible afin que vous puissiez ajouter un test si l'utilisateur est connecté.

5
var-foo

D'après ce que je peux voir, le problème de pourquoi votre code ne fonctionne pas est que vous n'avez pas injecté $ scope. Michiels répond fonctionne parce qu'il injecte $ rootScope et appelle le cycle de résumé. Cependant, votre $ apply () est un niveau supérieur d'invoquer le cycle de résumé, donc cela fonctionnera aussi ... MAIS! que si vous l'injectez dans le service lui-même.

Mais je pense qu'un service ne crée pas un enfant $ scope, vous devez donc injecter $ rootScope lui-même - pour autant que je sache, seuls les contrôleurs vous permettent d'injecter $ scope comme son travail pour bien créer un $ scope. Mais c'est un peu de spéculation, je n'en suis pas sûr à 100%. J'essaierais cependant avec $ rootScope car vous savez que l'application a un $ rootScope depuis la création de 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
Sten Muchow