$rootScope.digest not completing a promise in Jasmine
Asked Answered
R

1

9

We currently are trying to test our angular services which use promises to return values to the controllers. The issue is that the functions we attach to the .then do not get called in Jasmine.

We found that adding $rootScope.digest() to the function after the promise is returned allows synchronous promises to be called however it still does not work for Asynchronous promises.

The code so far is

    beforeEach(inject(function (Service, $rootScope)
    {
        service = Service;
        root = $rootScope;
    }));

    it('gets all the available products', function (done)
    {
        service.getData().then(function (data)
        {
            expect(data).not.toBeNull();
            expect(data.length).toBeGreaterThan(0);
            done();
        });
        root.$digest();
    });

In this case the promise gets called fine but if it is asynchronous it will not be called because the promise is not ready for "digestion" by the time the root.$digest() is called.

Is there some way to tell when a promise is after getting resolved so that i can call digest? Or maybe something that would do that automatically? Thanks ;)

Partial of the Service we must test(error handling removed):

var app = angular.module('service', []);

/**
 * Service for accessing data relating to the updates/downloads
 * */
app.factory('Service', function ($q)
{
     ... init

    function getData()
    {
        var deffered = $q.defer();

        var processors = [displayNameProc];

        downloads.getData(function (err, data)
        {
            chain.process(processors, data, function (err, processedData)
            {
                deffered.resolve(processedData);
            });
        });

        return deffered.promise;
    }
    ...

In the case where there is a problem is when service.getData is async the promise is resolved but the function attached to that promise from the test code is not called because the root.$digest has already been called. Hope this gives more info

Workaround

var interval;
beforeEach(inject(function ($rootScope)
{
    if(!interval)
    {
        interval = function ()
        {
            $rootScope.$digest();
        };
        window.setInterval(interval, /*timing*/);
    }
}));

I ended up using the workaround above to call the digest repeatedly so each layer of promise would get a chance to run...its not ideal or pretty but it works for the test...

Roughcast answered 22/9, 2015 at 13:14 Comment(6)
Are you mocking your service and therefore the returned promise?Shortening
No I was trying to test the actual service.Roughcast
If you're testing the actual service and it is internally using the $http service then I believe a digest will be kicked off by angularjs when the remote call succeeds or fails.Shortening
Yes i think that may be the case however unfortunately we are not using http requests for most of our services. At the moment i can add a hack to call digest repeatedly which kinda solves this but it is not a great solution. It would be great if there was some mechanism to tell when a promise was resolved but not digested.Roughcast
Any chance you can post more code or create a fiddle/plunk?Shortening
Just added a bit more code there maybe i could recreate the issue on fiddle but it would not be with the actual code we use because of the use of node webkitRoughcast
R
1

You should move your asserts outside of the then I believe:

beforeEach(inject(function (Service, $rootScope)
{
    service = Service;
    root = $rootScope;
}));

it('gets all the available products', function (done)
{
    var result;
    service.getData().then(function (data)
    {
        result = data;
        done();
    });

    root.$digest();

    expect(result ).not.toBeNull();
    expect(result .length).toBeGreaterThan(0);
});

Update

Forked the plunkr out of the comment below to show how you can test an async call.

Added a $timeout to the service:

var app = angular.module('bad-service', []);

app.service('BadService', function ($q, $interval, $timeout)
{
    this.innerPromise = function(){
      var task = $q.defer();
      console.log('Inside inner promise');

      $timeout(function() {
        console.log('Inner promise timer fired');
        task.resolve();
      }, 500);
      // $interval(function(){

      // }, 500, 1);
      return task.promise;
    }
});

And I am flushing the $timeout before asserting:

beforeEach(module('test'));


describe('test', function(){
  var service, root, $timeout;

    beforeEach(inject(function (Service, $rootScope, _$timeout_)
    {
      $timeout = _$timeout_;
        console.log('injecting');
        service = Service;
        root = $rootScope;
    }));

    /**
    Test one
    */
    it('Should work', function(done){
      console.log('Before service call');
      service.outerPromise().then(function resolve(){
        console.log('I should be run!');
        expect(true).toBeTruthy();
        done();
      });
      // You will have to use the correct "resolve" here. For example when using:
      // $httpbackend - $httpBackend.flush();
      // $timeout- $timeout.flush();
      $timeout.flush();

      console.log('After service call');
    });
});
Remark answered 23/12, 2015 at 10:59 Comment(8)
Unfortunately this wont work in our case because getData is async meaning that result will be null by the time it reaches the first expect(result ).not.toBeNull();Roughcast
This solution is specific for async calls; have you tested if it doesn't work? The combination of 'done();' and 'root.$digest();' will make sure that your async call gets resolved before the expects are called.Remark
Yes sorry, i see that now ill give it a shotRoughcast
Just gave it a shot there i think this is similar to what i had initially and unfortunetly it still wont call the then function because inside service.getData there is a nested promise call which is not 'digested' meaning that the outher one is never resolved, so what happens is 1) getData is called 2) digest is called 3)promise is not resolved due to nested promise 4) expect is called with null... afaik your solution should work but there must be something else going on i do not understand.Roughcast
Could you create a plunkr? Then I will have a look at what the issue might be.Remark
Added one there now: plnkr.co/edit/hVO1WjQrtIbfdYd0AAqE?p=preview Dont know if it covers our case exactly but it shows some of the issues we have been having.Roughcast
Plunkr does not seem to be working for me. I am getting the error: 'Uncaught ReferenceError: beforeEach is not defined'. This is because: 'Mixed Content: The page at 'run.plnkr.co/NSMWNpPB1Yb3hOrp' was loaded over HTTPS, but requested an insecure script 'cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js'. This request has been blocked; the content must be served over HTTPS.'. I fixed this by changing the 'http' to 'https' for the jasmine includesRemark
Yes i think that does the trick thanks :D...was stuck on that one for a while. Ill have to update all my hacked tests now but good to have a solution.Roughcast

© 2022 - 2024 — McMap. All rights reserved.