Подтвердить что ты не робот

AngularJS Promise Callback не запускается в JasmineJS Test

Ввод проблемы

Я пытаюсь использовать unit test службу AngularJS, которая обертывает Facebook JavaScript SDK FB объект; однако тест не работает, и я не смог понять, почему. Кроме того, код службы когда я запускаю его в браузере вместо JasmineJS тест, выполните Тест-драйв кармы.

Я тестирую асинхронный метод, используя Angular promises через $q объект. У меня есть тесты, настроенные для асинхронного запуска, используя Жасмин 1.3.1 методы асинхронного тестирования, но waitsFor() функция никогда не возвращает true (см. тестовый код ниже), это просто тайм-аут через 5 секунд. (Карма еще не поставляется с API асинхронного тестирования Jasmine 2.0).

Я думаю, это может быть потому, что метод then() обещание никогда не срабатывает (у меня есть console.log(), чтобы показать это), хотя я звоню $scope.$apply(), когда асинхронный метод возвращает, чтобы Angular знал, что он должен запускать дайджест цикл и вызвать обратный вызов then()... но я мог ошибаться.

Это вывод ошибки, полученный при запуске теста:

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)

Код

Это мой unit test для службы (см. встроенные комментарии, которые объясняют, что я нашел до сих пор):

'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

И это моя служба 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
    };
  }]);

Ресурсы

Вот список других ресурсов, на которые я уже рассмотрел попытайтесь решить эту проблему.

Резюме

Обратите внимание, что я знаю, что есть уже другие библиотеки Angular для SDK для JavaScript JavaScript, например:

Мне сейчас не интересно использовать их, потому что я хотел научиться писать службу Angular. Поэтому, пожалуйста, держите ответы ограниченными, помогая мне исправить проблемы в моем коде, вместо того, чтобы предлагать, чтобы я использовал чужую.

Итак, с учетом сказанного, знает кто, почему мой тест не работает?

4b9b3361

Ответ 1

TL; DR

Вызвать $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 здесь.

Примечание. Я удалил вызовы run() и wait(), потому что они здесь не нужны (никаких текущих асинхронных вызовов не выполняется).

Длительное объяснение

Здесь что происходит: когда вы вызываете getUserLoginStatus(), он внутренне запускает FB.getLoginStatus(), который, в свою очередь, выполняет свой обратный вызов немедленно, как и следовало ожидать, поскольку вы издевались над ним именно так. Но ваш вызов $scope.$apply() находится внутри этого обратного вызова, поэтому он запускается перед оператором .then() в тесте. И поскольку then() создает новое обещание, для этого обещания требуется новый дайджест. Чтобы решить проблему, выполните следующие действия.

Я считаю, что эта проблема не возникает в браузере из-за одной из двух причин:

  • FB.getLoginStatus() не вызывает обратный вызов немедленно, поэтому запускаются вызовы then(); или
  • Что-то еще в приложении запускает новый цикл дайджеста.

Итак, чтобы завершить это, если вы создаете обещание в рамках теста, явно или нет, вам нужно будет вызвать цикл дайджеста в какой-то момент, чтобы это обещание было разрешено.

Ответ 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

Это должно делать то, что вы ищете. Используя beforeEach для настройки rootScope и afterEach для запуска приложения, вы также легко расширяете свой тест, чтобы вы могли добавить тест, если пользователь вошел в систему.

Ответ 3

Из того, что я вижу, проблема в том, почему ваш код не работает, заключается в том, что вы не указали $scope. Michiels отвечает, потому что он вводит $rootScope и вызывает цикл дайджеста. Однако ваш $apply() - это более высокий уровень, вызывающий цикл дайджеста, поэтому он будет работать также... НО! только если вы введете его в саму службу.

Но я думаю, что служба не создает дочерний объект $scope, поэтому вам нужно вводить сам $rootScope - насколько я знаю, только контроллеры позволяют вам вводить $scope в качестве своей работы, чтобы хорошо создать область $scope. Но это немного спекуляция, я не уверен на 100 процентов. Я бы попробовал, однако, с $rootScope, поскольку вы знаете, что приложение имеет $rootScope от создания ng-приложения.

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