Promise.resolve().then not working in jasmine test - javascript

I'm trying to setup a test which involves promises. Here is my example code:
var promise;
beforeEach(inject(function ($q) {
promise = $q.resolve();
}));
it('should resolve', function (done) {
promise.then(function () {
expect(true).toBeTruthy();
done();
});
});
For some reason, when I run this, I get a TIMEOUT
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Why doesn't the promise execute the callback given to then ?
Cheers

You need to call scope/rootScope $digest method to resolve promises.
So it should be:
var result = false;
promise.then(function() { result = true;});
$rootScope.$digest();
expect(result).toBeTruthy();

Related

AngularJS Promises - Detecting when an Collection is Fully Filled [duplicate]

So I have a situation where I have multiple promise chains of an unknown length. I want some action to run when all the CHAINS have been processed. Is that even possible? Here is an example:
app.controller('MainCtrl', function($scope, $q, $timeout) {
var one = $q.defer();
var two = $q.defer();
var three = $q.defer();
var all = $q.all([one.promise, two.promise, three.promise]);
all.then(allSuccess);
function success(data) {
console.log(data);
return data + "Chained";
}
function allSuccess(){
console.log("ALL PROMISES RESOLVED")
}
one.promise.then(success).then(success);
two.promise.then(success);
three.promise.then(success).then(success).then(success);
$timeout(function () {
one.resolve("one done");
}, Math.random() * 1000);
$timeout(function () {
two.resolve("two done");
}, Math.random() * 1000);
$timeout(function () {
three.resolve("three done");
}, Math.random() * 1000);
});
In this example, I set up a $q.all() for promises one, two, and three which will get resolved at some random time. I then add promises onto the ends of one and three. I want the all to resolve when all the chains have been resolved. Here is the output when I run this code:
one done
one doneChained
two done
three done
ALL PROMISES RESOLVED
three doneChained
three doneChainedChained
Is there a way to wait for the chains to resolve?
I want the all to resolve when all the chains have been resolved.
Sure, then just pass the promise of each chain into the all() instead of the initial promises:
$q.all([one.promise, two.promise, three.promise]).then(function() {
console.log("ALL INITIAL PROMISES RESOLVED");
});
var onechain = one.promise.then(success).then(success),
twochain = two.promise.then(success),
threechain = three.promise.then(success).then(success).then(success);
$q.all([onechain, twochain, threechain]).then(function() {
console.log("ALL PROMISES RESOLVED");
});
The accepted answer is correct. I would like to provide an example to elaborate it a bit to those who aren't familiar with promise.
Example:
In my example, I need to replace the src attributes of img tags with different mirror urls if available before rendering the content.
var img_tags = content.querySelectorAll('img');
function checkMirrorAvailability(url) {
// blah blah
return promise;
}
function changeSrc(success, y, response) {
if (success === true) {
img_tags[y].setAttribute('src', response.mirror_url);
}
else {
console.log('No mirrors for: ' + img_tags[y].getAttribute('src'));
}
}
var promise_array = [];
for (var y = 0; y < img_tags.length; y++) {
var img_src = img_tags[y].getAttribute('src');
promise_array.push(
checkMirrorAvailability(img_src)
.then(
// a callback function only accept ONE argument.
// Here, we use `.bind` to pass additional arguments to the
// callback function (changeSrc).
// successCallback
changeSrc.bind(null, true, y),
// errorCallback
changeSrc.bind(null, false, y)
)
);
}
$q.all(promise_array)
.then(
function() {
console.log('all promises have returned with either success or failure!');
render(content);
}
// We don't need an errorCallback function here, because above we handled
// all errors.
);
Explanation:
From AngularJS docs:
The then method:
then(successCallback, errorCallback, notifyCallback) – regardless of when the promise was or will be resolved or rejected, then calls
one of the success or error callbacks asynchronously as soon as the
result is available. The callbacks are called with a single
argument: the result or rejection reason.
$q.all(promises)
Combines multiple promises into a single promise that is resolved when
all of the input promises are resolved.
The promises param can be an array of promises.
About bind(), More info here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Recently had this problem but with unkown number of promises.Solved using jQuery.map().
function methodThatChainsPromises(args) {
//var args = [
// 'myArg1',
// 'myArg2',
// 'myArg3',
//];
var deferred = $q.defer();
var chain = args.map(methodThatTakeArgAndReturnsPromise);
$q.all(chain)
.then(function () {
$log.debug('All promises have been resolved.');
deferred.resolve();
})
.catch(function () {
$log.debug('One or more promises failed.');
deferred.reject();
});
return deferred.promise;
}
There is a way. $q.all(...
You can check the below stuffs:
http://jsfiddle.net/ThomasBurleson/QqKuk/
http://denisonluz.com/blog/index.php/2013/10/06/angularjs-returning-multiple-promises-at-once-with-q-all/
You can use "await" in an "async function".
app.controller('MainCtrl', async function($scope, $q, $timeout) {
...
var all = await $q.all([one.promise, two.promise, three.promise]);
...
}
NOTE: I'm not 100% sure you can call an async function from a non-async function and have the right results.
That said this wouldn't ever be used on a website. But for load-testing/integration test...maybe.
Example code:
async function waitForIt(printMe) {
console.log(printMe);
console.log("..."+await req());
console.log("Legendary!")
}
function req() {
var promise = new Promise(resolve => {
setTimeout(() => {
resolve("DARY!");
}, 2000);
});
return promise;
}
waitForIt("Legen-Wait For It");

Why does this promise not behave as expected?

I'm struggling to understand why after calling updateStatus() I see 'promise resolved' logged to the console, but not 'refreshGames'. How is the promise resolved if the code inside refreshGames() never runs?
var refreshGames = function() {
console.log('refreshGames');
var defer = $q.defer();
playersService.getGames({
playerId: playerId
}).$promise.then(function(data) {
vm.games = data;
return defer.promise;
});
};
var updateStatus = function() {
$q.all([refreshGames.promise]).then(function() {
console.log('promise resolved');
populateOptions(vm.games);
vm.tableParams.reload();
});
};
Because your function refreshGames returns nothing, it should return the promise and the defer must be resolved, like this:
var refreshGames = function() {
console.log('refreshGames');
var defer = $q.defer();
playersService.getGames({
playerId: playerId
}).$promise.then(function(data) {
vm.games = data;
defer.resolve(data);
});
return defer.promise;
};
and in the $q.all you just do refreshGames()
refreshGames.promise is undefined - there's no code anywhere that creates this property on refreshGames
any non-promise in $q.all is promisified and effectively equivalent to Promise.resolve(n) (or however you do that with $q
so, your $q.all is essentially
$q.all([undefined]).then(function() {
...
});
and thus gets executed immediately

Jasmine 2.0 Test with Nested Angular Promise

I have a test that requires one promise to be run and later in its then handler another promise returning function is run.
The first promise resolves, and a successful call is made to the next function which returns a promise. However, the then handler for that second promise never fires.
Is there another way to test nested promises using Jasmine 2.0?
Example problem:
describe("nested promise suite", function () {
var prom1 = function () {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve('prom1');
}, 500)
return deferred.promise;
};
var prom2 = function () {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve('prom2');
}, 500);
return deferred.promise;
};
iit("nested promise test", function (done) {
prom1()
.then(function (result) {
console.log('prom1 result ', result);
prom2()
.then(function (result2) {
console.log('prom2 result ', result2);
})
.finally(function() {
console.log('you did it');
})
})
.finally(done); //this causes promise one to resolve properly but unsure of how to make the second promise resolve properly
$timeout.flush();
})
});
I'm not sure if this is the problem in your original code as well, but in this example, your second console.log doesn't fire because prom2 adds a new timeout after the first has been flushed. The promise from prom2 then waits for this new timeout to flush, which never happens:
prom1() is called
The first promise and timeout are created
The first timeout is flushed, resolving the first promise
The then() block is triggered, calling prom2()
The second promise and timeout are created
done() is called.
You can try adding a second $timeout.flush(); call right after the one that's already there, which will flush the second timeout, resolve the second promise, and log the missing messages.

How to Test Value Returned in Promise from AngularJS Controller with Jasmine?

I have a controller that expose a function that returns some text after a rest call. It works fine, but I'm having trouble testing it with Jasmine. The code inside the promise handler in the test never executes.
The controller:
/* global Q */
'use strict';
angular.module('myModule', ['some.service'])
.controller('MyCtrl', ['$scope', 'SomeSvc', function ($scope, SomeSvc) {
$scope.getTheData = function (id) {
var deferred = Q.defer();
var processedResult = '';
SomeSvc.getData(id)
.then(function (result) {
//process data
processedResult = 'some stuff';
deferred.resolve(processedResult);
})
.fail(function (err) {
deferred.reject(err);
});
return deferred.promise;
}
}]);
The test:
describe('some tests', function() {
var $scope;
var $controller;
var $httpBackend;
beforeEach(function() {
module('myModule');
inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
$scope = _$rootScope_.$new();
$controller = _$controller_;
$httpBackend = _$httpBackend_;
//mock the call that the SomeSvc call from the controller will make
$httpBackend.expect('GET', 'the/url/to/my/data');
$httpBackend.whenGET('the/url/to/my/data')
.respond({data:'lots of data'});
$controller ('MyCtrl', {
$scope: $scope
});
});
});
describe('test the returned value from the promise', function() {
var prom = $scope.getTheData(someId);
prom.then(function(result) {
expect(result).toBe('something expected'); //this code never runs
})
});
});
Anything inside a then will not be run unless the promise callbacks are called - which is a risk for a false positive like you experienced here. The test will pass here since the expect was never run.
There are many ways to make sure you don't get a false positive like this. Examples:
A) Return the promise
Jasmine will wait for the promise to be resolved within the timeout.
If it is not resolved in time, the test will fail.
If the promise is rejected, the test will also fail.
Beware If you forget the return, your test will give a false positive!
describe('test the returned value from the promise', function() {
return $scope.getTheData(someId)
.then(function(result) {
expect(result).toBe('something expected');
});
});
B) Use the done callback provided by Jasmine to the test method
If done is not called within the timeout the test will fail.
If done is called with arguments the test will fail.
The catch here will pass the error to jasmine and you will see the error
in the output.
Beware If you forget the catch, your error will be swallowed and your test will fail with a generic timeout error.
describe('test the returned value from the promise', function(done) {
$scope.getTheData(someId)
.then(function(result) {
expect(result).toBe('something expected');
done();
})
.catch(done);
});
C) Using spies and hand cranking (synchronous testing)
If you're not perfect this might be the safest way to write tests.
it('test the returned value from the promise', function() {
var
data = { data: 'lots of data' },
successSpy = jasmine.createSpy('success'),
failureSpy = jasmine.createSpy('failure');
$scope.getTheData(someId).then(successSpy, failureSpy);
$httpBackend.expect('GET', 'the/url/to/my/data').respond(200, data);
$httpBackend.flush();
expect(successSpy).toHaveBeenCalledWith(data);
expect(failureSpy).not.toHaveBeenCalled();
});
Synchronous testing tricks
You can hand crank httpBackend, timeouts and changes to scope when needed to get the controller/services to go one step further. $httpBackend.flush(), $timeout.flush(), scope.$apply().
In case there is a promise created by $q somewhere (and since you seem to be using $httpBackend, then this might well be the case), then my suggestions are to trigger a digest cycle after the call to getTheData, and make sure that the call to expect isn't in a then callback (so that if something is broken, the test fails, rather than just not run).
var prom = $scope.getTheData(someId);
$scope.$apply();
expect(result).toBe('something expected');
The reason this might help is that Angular promises are tied to the digest cycle, and their callbacks are only called if a digest cycle runs.
The problem you face is that you are testing async code (promise based) using synchronous code. That won't work, as the test has finished before the Thenable in your test code has started running the callback containing the test. The Jasmine docs specifically show how to test async code (same as Mocha), and the fix is very small:
describe('test the returned value from the promise', function(done) {
var prom = $scope.getTheData(someId);
prom.then(function(result) {
expect(result).toBe('something expected'); //this code never runs
done(); // the signifies the test was successful
}).catch(done); // catches any errors
});

How do I unit test an AngularJS controller that relies on a promise?

In my controller, I have:
$scope.index = function() {
CompanyService.initialized.then(function() {
var company_id = CompanyService.getCompany()._id;
LocationService.list(company_id, $routeParams.location_parent_id).then(function(response) {
if(response.data.status === 'ok') {
$scope.locations = response.data.locations;
$scope.current_location = response.data.location || null;
}
});
});
}
So it should get the LocationService list and the test is as follows:
it('should get locations', function() {
$httpBackend.when('GET', '/api/v1/location/list').respond({status: 'ok', locations: ['loc1', 'loc2']})
$scope.index();
expect($scope.locations.length).toEqual(2);
However, this never happens, because CompanyService has a promise that never gets resolved in the unit test. How do I either mock the returned promise of CompanyService or bypass it?
Simply mock the call using createSpyObj method from Jasmine:
describe('one test', function(){
var deferred, CompanyService, $scope;
beforeEach(inject(function ($q, $rootScope, $controller) {
params = {
$scope: $rootScope.$new(),
CompanyService = jasmine.createSpyObj('CompanyService', ['initialized'])
}
params.CompanyService.initialized.andCallFake(function () {
deferred = $q.defer();
return deferred.promise; //will fake to return a promise, in order to reach it inside the test
});
$controller('YourController', params);
});
it('my test', function(){
deferred.resolve({}); //empty object returned for the sample but you can set what you need to be returned
$scope.$digest(); //important in order to resolve the promise
//at this time, the promise is resolved! so your logic to test can be placed here
});
});
Short answer: Add $scope.$apply(); after calling $scope.$index();.
Long answer: Your problem is probably due to the way $q propagates promises resolution. Indeed resolution only happens when $digest is performed. I don't know why $digest is not performed during tests whereas it is during regular use, but this solution is suggested anyway in the Angular documentation here: https://docs.angularjs.org/api/ng/service/$q .
What's more you could directly use $scope.$digest(); in your case instead of $scope.$apply(); since $apply is only a wrapper that runs any function passed to it before calling $digest.

Categories

Resources