I'm trying to set a timeout in my controller so that if a response isn't received in 250ms it should fail. I've set my unit test to have a timeout of 10000 so that this condition should be met,Can anyone point me in the right direction? ( EDIT I'm trying to achieve this without using the $http service which I know provides timeout functinality)
(EDIT - my other unit tests were failing because I wasn't calling timeout.flush on them, now I just need to get the timeout message kicking in when an undefined promise is returned by promiseService.getPromise(). I've removed the early code from the question) .
promiseService (promise is a test suite variable allowing me to use different behaviour for the promise in each test suite before apply, eg reject in one, success in another)
mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
mockPromiseService.getPromise.andCallFake( function() {
promise = $q.defer();
return promise.promise;
})
Controller function that's being tested -
$scope.qPromiseCall = function() {
var timeoutdata = null;
$timeout(function() {
promise = promiseService.getPromise();
promise.then(function (data) {
timeoutdata = data;
if (data == "promise success!") {
console.log("success");
} else {
console.log("function failure");
}
}, function (error) {
console.log("promise failure")
}
)
}, 250).then(function (data) {
if(typeof timeoutdata === "undefined" ) {
console.log("Timed out")
}
},function( error ){
console.log("timed out!");
});
}
Test (normally I resolve or reject the promise in here but by not setting it I'm simulating a timeout)
it('Timeout logs promise failure', function(){
spyOn(console, 'log');
scope.qPromiseCall();
$timeout.flush(251);
$rootScope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
})
First, I would like to say that your controller implementation should be something like this:
$scope.qPromiseCall = function() {
var timeoutPromise = $timeout(function() {
canceler.resolve(); //aborts the request when timed out
console.log("Timed out");
}, 250); //we set a timeout for 250ms and store the promise in order to be cancelled later if the data does not arrive within 250ms
var canceler = $q.defer();
$http.get("data.js", {timeout: canceler.promise} ).success(function(data){
console.log(data);
$timeout.cancel(timeoutPromise); //cancel the timer when we get a response within 250ms
});
}
Your tests:
it('Timeout occurs', function() {
spyOn(console, 'log');
$scope.qPromiseCall();
$timeout.flush(251); //timeout occurs after 251ms
//there is no http response to flush because we cancel the response in our code. Trying to call $httpBackend.flush(); will throw an exception and fail the test
$scope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
})
it('Timeout does not occur', function() {
spyOn(console, 'log');
$scope.qPromiseCall();
$timeout.flush(230); //set the timeout to occur after 230ms
$httpBackend.flush(); //the response arrives before the timeout
$scope.$apply();
expect(console.log).not.toHaveBeenCalledWith("Timed out");
})
DEMO
Another example with promiseService.getPromise:
app.factory("promiseService", function($q,$timeout,$http) {
return {
getPromise: function() {
var timeoutPromise = $timeout(function() {
console.log("Timed out");
defer.reject("Timed out"); //reject the service in case of timeout
}, 250);
var defer = $q.defer();//in a real implementation, we would call an async function and
// resolve the promise after the async function finishes
$timeout(function(data){//simulating an asynch function. In your app, it could be
// $http or something else (this external service should be injected
//so that we can mock it in unit testing)
$timeout.cancel(timeoutPromise); //cancel the timeout
defer.resolve(data);
});
return defer.promise;
}
};
});
app.controller('MainCtrl', function($scope, $timeout, promiseService) {
$scope.qPromiseCall = function() {
promiseService.getPromise().then(function(data) {
console.log(data);
});//you could pass a second callback to handle error cases including timeout
}
});
Your tests are similar to the above example:
it('Timeout occurs', function() {
spyOn(console, 'log');
spyOn($timeout, 'cancel');
$scope.qPromiseCall();
$timeout.flush(251); //set it to timeout
$scope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
//expect($timeout.cancel).not.toHaveBeenCalled();
//I also use $timeout to simulate in the code so I cannot check it here because the $timeout is flushed
//In real app, it is a different service
})
it('Timeout does not occur', function() {
spyOn(console, 'log');
spyOn($timeout, 'cancel');
$scope.qPromiseCall();
$timeout.flush(230);//not timeout
$scope.$apply();
expect(console.log).not.toHaveBeenCalledWith("Timed out");
expect($timeout.cancel).toHaveBeenCalled(); //also need to check whether cancel is called
})
DEMO
The behaviour of "failing a promise unless it is resolved with a specified timeframe" seems ideal for refactoring into a separate service/factory. This should make the code in both the new service/factory and controller clearer and more re-usable.
The controller, which I've assumed just sets the success/failure on the scope:
app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) {
failUnlessResolvedWithin(function() {
return myPromiseService.getPromise();
}, 250).then(function(result) {
$scope.result = result;
}, function(error) {
$scope.error = error;
});
});
And the factory, failUnlessResolvedWithin, creates a new promise, which effectively "intercepts" a promise from a passed in function. It returns a new one that replicates its resolve/reject behaviour, except that it also rejects the promise if it hasn't been resolved within the timeout:
app.factory('failUnlessResolvedWithin', function($q, $timeout) {
return function(func, time) {
var deferred = $q.defer();
$timeout(function() {
deferred.reject('Not resolved within ' + time);
}, time);
$q.when(func()).then(function(results) {
deferred.resolve(results);
}, function(failure) {
deferred.reject(failure);
});
return deferred.promise;
};
});
The tests for these are a bit tricky (and long), but you can see them at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . The main points of the tests are
The tests for the controller mocks failUnlessResolvedWithin with a call to $timeout.
$provide.value('failUnlessResolvedWithin', function(func, time) {
return $timeout(func, time);
});
This is possible since 'failUnlessResolvedWithin' is (deliberately) syntactically equivalent to $timeout, and done since $timeout provides the flush function to test various cases.
The tests for the service itself uses calls $timeout.flush to test behaviour of the various cases of the original promise being resolved/rejected before/after the timeout.
beforeEach(function() {
failUnlessResolvedWithin(func, 2)
.catch(function(error) {
failResult = error;
});
});
beforeEach(function() {
$timeout.flush(3);
$rootScope.$digest();
});
it('the failure callback should be called with the error from the service', function() {
expect(failResult).toBe('Not resolved within 2');
});
You can see all this in action at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview
My implementation of #Michal Charemza 's failUnlessResolvedWithin with a real sample.
By passing deferred object to the func it reduces having to instantiate a promise in usage code "ByUserPosition". Helps me deal with firefox and geolocation.
.factory('failUnlessResolvedWithin', ['$q', '$timeout', function ($q, $timeout) {
return function(func, time) {
var deferred = $q.defer();
$timeout(function() {
deferred.reject('Not resolved within ' + time);
}, time);
func(deferred);
return deferred.promise;
}
}])
$scope.ByUserPosition = function () {
var resolveBy = 1000 * 30;
failUnlessResolvedWithin(function (deferred) {
navigator.geolocation.getCurrentPosition(
function (position) {
deferred.resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude });
},
function (err) {
deferred.reject(err);
}, {
enableHighAccuracy : true,
timeout: resolveBy,
maximumAge: 0
});
}, resolveBy).then(findByPosition, function (data) {
console.log('error', data);
});
};
Related
In my angularjs app I have the following code on button click:
if (!$scope.isChecked) {
$scope.getExistingName($scope.userName).then(function (data) {
$scope.userName = data;
});
}
//some processing code here then another promise
myService.save($scope.userName,otherparams).then(function (res) {
//route to page
}, function (err) {
});
The issue here is if $scope.isChecked is false, it goes inside the promise and since it takes time to resolve it comes out and goes to next line of code. Because of this $scope.userName is not updated and uses old values instead of the updated value returned.
Whats the best way to handle this?
You can use $q. First you have to inject $q in your angular controller.
//Create empty promise
var promise;
if (!$scope.isChecked) {
promise = $scope.getExistingName($scope.userName).then(function (data) {
$scope.userName = data;
});
}
// Wait or not for your promise result, depending if promise is undefined or not.
$q.all([promise]).then(function () {
//some processing code here then another promise
myService.save($scope.userName,otherparams).then(function (res) {
//route to page
}, function (err) {
});
});
If the myService.save need to wait for the $scope.getExistingName promise just compute that operation inside the "then" statement.
if (!$scope.isChecked) {
$scope.getExistingName($scope.userName).then(function (data) {
$scope.userName = data;
myService.save($scope.userName,otherparams).then(function (res) {
//route to page
}, function (err) {
});
});
}
//some processing code here then another promise
I have an Angular service that makes a call to the server and fetch the user list. The service returns a Promise.
Problem
Promise is not being resolved until and unless I call $rootScope.$digest(); either in the service or in the test itself.
setTimeout(function () {
rootScope.$digest();
}, 5000);
Apparently, Calling $rootScope.$digest(); is a workaround and I cannot call it in the angular service so I am calling it in the unit test with an interval of 5 seconds which I think is a bad practice.
Request
Please suggest an actual solution for this.
Given below is the test that I have written.
// Before each test set our injected Users factory (_Users_) to our local Users variable
beforeEach(inject(function (_Users_, $rootScope) {
Users = _Users_;
rootScope = $rootScope;
}));
/// test getUserAsync function
describe('getting user list async', function () {
// A simple test to verify the method getUserAsync exists
it('should exist', function () {
expect(Users.getUserAsync).toBeDefined();
});
// A test to verify that calling getUserAsync() returns the array of users we hard-coded above
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
///WORK AROUND
setTimeout(function () {
rootScope.$digest();
}, 5000);
});
})
service
Users.getUserAsync = function () {
var defered = $q.defer();
$http({
method: 'GET',
url: baseUrl + '/users'
}).then(function (response) {
defered.resolve(response);
}, function (response) {
defered.reject(response);
});
return defered.promise;
}
You can cause the promises to flush with a call to $timeout.flush(). It makes your tests a little bit more synchronous.
Here's an example:
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
$timeout.flush();
});
Aside: the failback won't be handled so it adds additional complexity to the test.
I am trying to delete a post from a list. The delete function is performing by passing serially to a delete function showed below.
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) { });
}
After performing the function, I need to reload the http.get request which used for listing the list.
$http.get("api/phone_accept.php")
.then(function (response) { });
Once the function performed. The entire list will reload with new updated list. Is there any way to do this thing.
Try this
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) {
//on success of first function it will call
$http.get("api/phone_accept.php")
.then(function (response) {
});
});
}
function list_data() {
$http.get("api/phone_accept.php")
.then(function (response) {
console.log('listing');
});
}
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) {
// call function to do listing
list_data();
});
}
Like what #sudheesh Singanamalla says by calling the same http.get request again inside function resolved my problem.
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref).success(function (data) {
//same function goes here will solve the problem.
});}
});
You can use $q - A service that helps you run functions asynchronously, and use their return values (or exceptions) when they are done processing.
https://docs.angularjs.org/api/ng/service/$q
Inside some service.
app.factory('SomeService', function ($http, $q) {
return {
getData : function() {
// the $http API is based on the deferred/promise APIs exposed by the $q service
// so it returns a promise for us by default
return $http.get("api/phone_recev.php?id="+ref)
.then(function(response) {
if (typeof response.data === 'object') {
return response.data;
} else {
// invalid response
return $q.reject(response.data);
}
}, function(response) {
// something went wrong
return $q.reject(response.data);
});
}
};
});
function somewhere in controller
var makePromiseWithData = function() {
// This service's function returns a promise, but we'll deal with that shortly
SomeService.getData()
// then() called when gets back
.then(function(data) {
// promise fulfilled
// something
}, function(error) {
// promise rejected, could log the error with: console.log('error', error);
//some code
});
};
I'm trying to submit a form that require that the user email is not duplicated, but I want to make an small animation before the POST request. In the $scope.process function I'm getting:
TypeError: Cannot read property 'catch' of undefined.
That's happening because $scope.process is returning before the $http.post is complete, but how can I make process() return the promise instead of undefined?
So, this is what I have so far:
//The submit form function
$scope.submitForm = function () {
$('.btn').attr('disable');
if ($scope.form.$valid) {
$scope.process($scope.account)
.catch(function (err) {
if (err.code === 'duplicate') {
// handle error
}
});
return false;
}
};
//This is the one in charge to send the request
$scope.process = function(body) {
// Timeout before http post to wait for animation
$timeout(function() {
return $http.post(postUrl, body).then(function (response) {
// This return a promise if I remove the $timeout
var nextPage = response.data;
}).catch(function (err) {
throw err;
});
}, 300);
// Return undefined due to $timeout
};
Thanks in advance.
You were getting TypeError: Cannot read property 'catch' of undefined, because you weren't returning promise from process function at all.
Do return $timeout promise from process function & apply .then & .catch over $timeout promise object.
By returning $timeout service the inner $http.post will return a data, so that will make proper chaining mechanism.
Code
$scope.process = function(body) {
// returned promise from here
return $timeout(function() {
//returned $http promise from here.
return $http.post(postUrl, body).then(function (response) {
// This return a promise if I remove the $timeout
nextPage = response.data;
return nextPage; //return data from here will return data from promise.
}).catch(function (err) {
throw err;
});
}, 300);
};
i have a factory am passing it to controller LoginCtrl
.factory('Fbdata', function(){
var service = {
data: {
apiData: []
},
login: function () {
facebookConnectPlugin.login(["email"],
function() {
facebookConnectPlugin.api("me/?fields=id,email,name,picture", ["public_info","user_birthday"],
function (results) {
service.data.apiData = results;
console.log(service.data.apiData);
return results;
},
function (error) {
console.error('FB:API', error);
});
},
function(err) {
console.error('FB:Login', err);
});
}
};
return service;
})
LoginCtrl:
.controller('LoginCtrl', function($scope, Fbdata){
$scope.login = function(){
if (!window.cordova) {
var appId = "appId";
facebookConnectPlugin.browserInit(appId);
}
$scope.loginData = Fbdata.login();
console.log(Fbdata.data.apiData);
// got empty array []
$scope.retVal= angular.copy(Fbdata.data.apiData);
};
})
the Fbdata.data.apiData return empty array and i only could see the returned data from the login success function in the console .
my template which is has LoginCtrl as controller:
<div class="event listening button" ng-click="login();">Login with Facebook</div>
<h2>{{loginData.name}}</h2>
<h2>{{retVal.name}}</h2>
There is a variety of ways to achieve this, example:
Now I have never used Cordova Facebook Plugin so I'm not sure if you need to run the api function after the log in, or how those procedures need to be ordered. But I wanted to show you an example of how to retrieve the data from the factory using your code sample. Hope that helps
Edit 2:
I have changed my code to using promises that way we make sure that we don't call one without the other being completed, I am not a fan of chaining the login and api functions within one function since it is possible(?) that you may need to call login() but don't want to call api(), please try my code and paste in your console logs in the bottom of your question.
Factory:
// Let's add promises to our factory using AngularJS $q
.factory('Fbdata', ['$q', function($q){
// You could also just replace `var service =` with `return` but I thought this
// would make it easier to understand whats going on here.
var service = {
// I generally nest result to keep it clean when I log
// So results from functions that retrieve results are stored here
data: {
login: [],
api: []
},
api: function() {
var q = $q.defer();
facebookConnectPlugin.api("me/?fields=id,email,name,picture", ["public_info","user_birthday"],
function (results) {
// assign to the object being returned
service.data.api = results;
// The data has returned successfully so we will resolve our promise
q.resolve(results);
},
function (error) {
// We reject here so we can inform the user within through the error/reject callback
q.reject(error);
console.error('FB:API', error);
});
// Now that we have either resolved or rejected based on what we received
// we will return the promise
return q.promise;
},
login: function () {
var q = $q.defer();
facebookConnectPlugin.login(["email"], function (results) {
// assign to the object being returned
service.data.login = results;
q.resolve(results);
}, function(err) {
q.reject(error);
console.error('FB:Login', err);
});
return q.promise;
}
};
return service;
}])
Controller:
.controller('LoginCtrl', function($scope, Fbdata){
$scope.login = function(){
if (!window.cordova) {
var appId = "appid";
facebookConnectPlugin.browserInit(appId);
}
// By using the promises in our factory be can ensure that API is called after
// login by doing the following
// then(success, reject) function allows us to say once we have a return, do this.
Fbdata.login().then(function () {
$scope.loginData = Fbdata.data.login;
// Check what was returned
console.log('loginData', $scope.loginData);
Fbdata.api().then(function () {
$scope.apiData = Fbdata.data.api;
console.log('apiData', $scope.apiData);
}, function () {
// Tell the user that the api failed, if necessary
});
}, function () {
// Tell the user that the log in failed
});
};
});