javascript 在 JasmineJS 测试中未触发 AngularJS 承诺回调

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21616766/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 21:19:28  来源:igfitidea点击:

AngularJS Promise Callback Not Trigged in JasmineJS Test

javascriptangularjsjasminekarma-runner

提问by Michael Benford

Problem Intro

问题介绍

I'm trying to unit test an AngularJS service that wraps the Facebook JavaScript SDKFBobject; 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 JasmineJSunit test, run with Karma test runner.

我正在尝试对包装Facebook JavaScript SDKFB对象的 AngularJS 服务进行单元测试;但是,测试不起作用,我一直无法弄清楚原因。此外,当我在浏览器中运行服务代码而不是JasmineJS单元测试时,服务代码确实有效,使用Karma test runner运行。

I'm testing an asynchronous method using Angular promises via the $qobject. 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).

我正在通过$q对象使用 Angular 承诺测试异步方法。我将测试设置为使用Jasmine 1.3.1 异步测试方法异步运行,但waitsFor()函数永远不会返回true(请参阅下面的测试代码),它只是在 5 秒后超时。(Karma 尚未附带 Jasmine 2.0 异步测试 API)。

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.

我认为这可能是因为then()promise的方法从未被触发(我有一个console.log()设置来证明这一点),即使我$scope.$apply()在异步方法返回时调用,让 Angular 知道它应该运行一个摘要循环并触发then()回调......但我可能是错的。

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):

这是我正在测试的 Angular 服务(请参阅解释我目前发现的内容的内联注释):

'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:

请注意,我知道 Facebook JavaScript SDK 已经有其他 Angular 库,例如:

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.

我现在对使用它们不感兴趣,因为我想学习如何自己编写 Angular 服务。因此,请将答案仅限于帮助我解决代码中的问题,而不是建议我使用其他人的.

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

那么,话虽如此,有谁知道为什么我的测试不起作用?

回答by Michael Benford

TL;DR

TL; 博士

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

$rootScope.$digest()从您的测试代码调用,它会通过:

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

注意:我删除run()wait()调用是因为这里不需要它们(没有执行实际的异步调用)。

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 beforethe .then()statement in the test. And since then()creates a new promise, a new digest is required for that promise to get resolved.

正在发生的事情是这样的:当您调用 时getUserLoginStatus(),它会在内部运行FB.getLoginStatus(),而后者会立即执行其回调,正如它应该的那样,因为您已经对其进行了模拟以精确执行此操作。但是您的$scope.$apply()调用在该回调中,因此它会在测试中的语句之前执行.then()。并且由于then()创建了一个新的承诺,因此需要一个新的摘要才能解决该承诺。

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.
  1. FB.getLoginStatus()不会立即调用它的回调,所以任何then()调用都会先运行;或者
  2. 应用程序中的其他内容会触发新的摘要循环。

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.

因此,总而言之,如果您在测试中创建了一个承诺,无论是否明确,您都必须在某个时候触发一个摘要循环,以便该承诺得到解决。

回答by var-foo

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

这应该做你正在寻找的。通过使用 beforeEach 设置您的 rootScope 并使用 afterEach 来运行应用程序,您还可以轻松扩展您的测试,以便您可以添加用户是否登录的测试。

回答by Sten Muchow

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.

从我可以看出为什么您的代码不起作用的问题是您没有注入 $scope。Michiels 的回答有效,因为他注入了 $rootScope 并调用了摘要循环。但是,您的 $apply() 是调用摘要循环的更高级别,因此它也可以正常工作......但是!仅当您将其注入服务本身时。

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.

但我认为服务不会创建 $scope 子项,因此您需要注入 $rootScope 本身 - 据我所知,只有控制器允许您注入 $scope 作为其创建 $scope 的工作。但这只是一种推测,我不是 100% 确定。但是,我会尝试使用 $rootScope,因为您知道该应用程序从 ng-app 的创建中获得了一个 $rootScope。

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