Unit Testing angular $httpBackend service - angularjs

I've got right now a project were we need to have the backend server mocked for the time being and we are using $httpBackend on the application .run feature. I need to unit test this service that contains the $httpBackend as we will be having a vast amount of mocked calls to the server we will be covering. So right now this is what I have. As a preface to my question the current setup works when I call mockDataService.getWorkflowTask from a controller on a simple page.
My Server replacement service:
angular.module('app').run(function ($httpBackend, $resource, FakeBackendService) {
// TODO: add all necessary http intercepts.
$httpBackend.whenGET('JSON file').respond(function (method, url, data) {
var request = new XMLHttpRequest();
request.open('GET', url, false);
request.send(null);
return [request.status, request.response, {}];
});
$httpBackend.whenGET(/.*/).respond(function (method, url, data) {
return [200, FakeBackendService.getWorkflowTasks(), {}];
});
});
Here is the service for FakeBackendService:
(function () {
'use strict';
var injectParams = [];
function service(lodash) {
var vm = this;
var ret = {
getWorkflowTasks: getWorkflowTasks
};
function getWorkflowTasks() {
if (vm.workflowtasks.length < 1) {
vm.workflowtasks = loadWorkflowTasks("Some JSON file");
}
return vm.workflowtasks;
};
function loadWorkflowTasks(file) {
var workflowTasks = [];
var request = new XMLHttpRequest();
request.open("GET", file, false);
request.send(null);
if (request.status == 200) {
workflowTasks = angular.fromJson(request.response);
}
return workflowTasks;
};
function init() {
vm.workflowtasks = [];
}
init();
return ret;
}
service.$inject = injectParams;
angular.module('mock.FakeBackendService', []).service('FakeBackendService', service);
})();
So that is currently the backend server replacement mock. The following is my data handling service which contains the call to $http.get(blah blah blah).
(function () {
'use strict';
var injectParams = ['$http', '$q', 'mockConfigService', '$httpBackend'];
function factory($http, $q, configService, $httpBackend) {
var vm = this;
var factory = {
getWorkflowTask: getWorkflowTask
};
function getWorkflowTask(str) {
return getResource(str);
}
function init() {
// Get the URL we will be using to get data from
vm.dataServiceURL = configService.getDataServiceURL();
}
function getResource(baseResource) {
var resource = vm.dataServiceURL + baseResource;
return $http.get(resource).then(function (response) {
if (typeof response.data == 'object') {
// Got valid response
return $q.resolve(response.data);
}
else {
// Invalid response
return $q.reject(response.data);
}
}, function (response) {
// Something went wrong
return $q.reject(response.data);
});
}
init();
return factory;
};
factory.$inject = injectParams;
angular.module('mock.dataService', []).factory('mockDataService', factory);
}());
Now for the Jasmine-Karma Unit test.
describe("HTTP Backend Mock testing", function () {
beforeEach(angular.mock.module("app"));
beforeEach(angular.mock.module("mock.FakeBackendService"));
beforeEach(angular.mock.module("mock.configService"));
beforeEach(angular.mock.module("mock.dataService"));
it("Get the workflow task", angular.mock.inject(function (mockDataService) {
var valid = "";
var promise = mockDataService.getWorkflowTask('http://localhost/foo');
promise.then(function (response) {
valid = "Success";
}, function (response) {
valid = "Failure";
});
expect(valid).toBe("Success");
}));
});
Now to the question. So, I'll start by saying I'm new to the AngularJS world and even more so to Jasmine. Anyways, when I debug the unit test I find that the promise's status is still 0 and I always get expected '' to be 'Success' telling my I never resolve (hopefully I'm using the right lingo) the promise from the $http service in mockDataService. I've tried playing around with it some and tried to see if anyone has done this kind of a thing before. I found plenty of examples where the $httpBackend is mocked in the test but none like what I'm attempting. Any ideas or suggestions would be great. Thanks.
EDIT got a slightly working solution
So I decided that I'd by pass the run() service and just do the same response in the expectGET().respond().
describe("HTTP Backend Mock testing", function () {
beforeEach(angular.mock.module("app"));
beforeEach(angular.mock.module("mock.FakeBackendService"));
beforeEach(angular.mock.module("mock.configService"));
beforeEach(angular.mock.module("mock.dataService"));
it("Get the workflow task", angular.mock.inject(function (mockDataService, $httpBackend, FakeBackendService) {
var valid = "";
$httpBackend.expectGET('http://server:80/api/foo').respond(200, FakeBackendService.getWorkflowTasks());
var promise = mockDataService.getWorkflowTask('foo');
promise.then(function (response) {
valid = "Success";
}, function (response) {
valid = "Failure";
});
$httpBackend.flush();
expect(valid).toBe("Success");
}));
});
This sort of solves my testing problem with the run() as the goal was to verify 1) That the regex matching call the correct FakeBackendService and 2) That FakeBackendService returns correct file and actually loads it. I think I can do that by mimicking the same regex in the expectGET. However, I'll leave open for a bit to see if anyone knows how to get the run() to work.

The promise is not going to resolve unless you force it to do so before the test ends. Here is one such way to do it:
$httpBackend.expectGET(......).respond(200, 'abc');
var promise = mockDataService.getWorkflowTask('http://localhost/foo');
promise.then(function (response) {
valid = "Success";
}, function (response) {
valid = "Failure";
});
//new code here
$httpBackend.flush();
expect(valid).toBe("Success");
This will force the promise to resolve and your test should pass. You'll also need to inject the $httpBackend service into the test.

angular.module('mock.dataService', [])
.service('mockDataService', function($http) {
this.getWorkflowTask = function(url) {
return $http.get(url)
}
})
describe('HTTP Backend Mock testing', function() {
var $httpBackend
beforeEach(angular.mock.module("mock.dataService"));
beforeEach(inject(function(_$httpBackend_) {
$httpBackend = _$httpBackend_
}))
it("Get the workflow task", angular.mock.inject(function(mockDataService) {
$httpBackend.expectGET('http://localhost/foo').respond(200);
var promise = mockDataService.getWorkflowTask('http://localhost/foo');
promise.then(function(response) {
valid = "Success";
}, function(response) {
valid = "Failure";
});
$httpBackend.flush();
expect(valid).toBe("Success");
}));
})
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Related

mock HTTP response with promises and return dummy data

I am calling an API service which returns a promise from a factory.
Here is a part of my factory.
factories.factory('OnBoardingFactory', ['$http',
function ($http) {
var dataFactory = {};
dataFactory.get = function (url) {
return $http.get('http://localhost/api/onboarding/' + url)
};
return dataFactory
}
]);
And here is where its called from the controller:
OnBoardingFactory.get('login?username=test&password=password')
.then(function(response){
$scope.response = response.status;
})
This returns data in the controller absolutely fine. However I have difficulties when I come to test it. Here is my test script:
var scope, FakeOnBoardingFactory, controller, q, deferred;
beforeEach(module('app.module'));
beforeEach(function () {
FakeOnBoardingFactory = {
get: function () {
deferred = q.defer();
// Place the fake return object here
deferred.resolve({ response: {status: 200}});
return deferred.promise;
}
};
spyOn(FakeOnBoardingFactory, 'get').and.callThrough();
});
beforeEach(inject(function ($q, $rootScope, $controller, $injector ) {
scope = $rootScope.$new();
q = $q;
controller = $controller(OnBoardingCtrl, {
$scope: scope,
OnBoardingFactory: FakeOnBoardingFactory
})
}));
it('Should call the form and return 200', function () {
// Execute form
scope.loginCredentials({$valid: true});
scope.$apply();
// Ensure script is called (which passes fine)
expect(FakeOnBoardingFactory.get).toHaveBeenCalled();
scope.$apply();
// BREAKS HERE
expect(scope.status).toBe(200);
})
When expect(FakeOnBoardingFactory.get).toHaveBeenCalled(); is called, this passes fine. However then I run expect(scope.status).toBe(200), it breaks "Expected undefined to be 200".
This would indicate that my FakeOnBoardingFactory isn't returning any data. But I can't seem to find the issue.
It must be the change to support multiple body assertions that has caused this bug.
The workaround for now is to either don't use expect and do your assertion in the end function callback.
So instead of .expect(200) it would be.
end(function(err,res) { res.status.should.equal(200) },
or if you do use expect.. you need to make sure you specify a body as well as just a status..
it('should assert status only 1', function(done){
var app = express();
app.get('/user', function(req, res){
res.send(201, { name: 'tobi' }); }); request(app) .get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '20')
.expect(201)
.end(function(err, res){
if (err) throw err;
});
})

Angular Unit Testing Service with multiple service calls using $q.all

Ok here is the scenario we are trying to unit test with Jasmine. We have a service defined similar to the service below:
(function () {
'use strict';
angular.module('mymodule')
.service('myservice', myservice);
myservice.$inject = ['$q', '$resource', 'progressService',
'myservice2', 'myservice3', 'ngDialog'];
function myservice($q, $resource, progressService,
myservice2, myservice3, ngDialog) {
var self = this;
self.dataListSvc2 = [];
self.dataListSvc3 = [];
self.dataFromResource = null;
self.myRoutine = myRoutine;
var myResource = $resource('/someurl/webapi/GetData');
//TRYING TO TEST THIS ROUTINE!!!
function myRoutine(param1, param2) {
return progressService.show($q.all([
myResource.get({ Param1: param1 }).$promise.then(function (response) {
self.dataFromResource = response;
}),
myRoutine2(param2),
myRoutine3(param2)
]));
}
function myRoutine2(param) {
return myservice2.getSomeData(param).then(function (response) {
var results = [];
self.dataListSvc2 = [];
response.forEach(function (item) {
item.AddField1 = null;
item.AddField2 = false;
results.push(item);
});
self.dataListSvc2 = results;
});
}
function myRoutine3(param) {
return myservice3.getSomeMoreData(param, [6])
.then(function (response) {
self.dataListSvc3 = response;
});
}
}
})();
I am trying to write a unit test for myservice.myRoutine() and not having any luck, I suspect it is due to the $q.all array of promises. Here is a test I settled on but it is not ideal and honestly I don't feel like it's testing anything of value. I have also tried "mocking" the three requests with $httpBackend with no luck as all three responses come back in an array with undefined values. I searched around SO and the web and I am only finding $q.all([]) unit tests referencing controllers but not services. If anyone has some input it would be much appreciated. Here is where I settled for the time being:
describe('My Service: ', function () {
var $httpBackend;
beforeEach(module('mymodule'));
beforeEach(inject(function ($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
}));
it('Can call myroutine from myservice', inject(function (myservice) {
//Arrange
var expectedVal1 = 1234;
var expectedVal2 = 1234;
spyOn(myservice, "myRoutine");
//Act
myservice.myRoutine(expectedVal1, expectedVal2);
//Assert
expect(myservice.myRoutine).toHaveBeenCalled();
}));
});
Use the $httpBackend.when('GET', '/someurl/webapi/GetData').respond(data); to fake the http requests. You should fake the myservice2.getSomeData method too and the myservice3.getSomeMoreData I guess those methods create http request too so you could fake them in the same manner.
expect(myservice.myRoutine).toHaveBeenCalled();
Is irrelevant as you called it manually ;-).
var fixture = {/*some data ... */};
it('Can call myroutine from myservice', inject(function (myservice) {
$httpBackend.when('GET', '/someurl/webapi/GetData').respond(fixture);
//Act
myservice.myRoutine(expectedVal1, expectedVal2);
$rootScope.$apply()
//Assert
expect(myservice.dataFromResource).toBe(fixture);
}));

Why unit testing response is always successful?

I was reading posts related for don't repeat the question.
I have the next unit testing code:
describe('service', function() {
var questionApiService;
beforeEach(module('myApp'));
beforeEach(inject(function (_questionApiService_) {
questionApiService = _questionApiService_;
}));
// Test service availability
it('check the existence of get field question service', inject(function(questionApiService) {
//expect(1).toEqual(100);
questionApiService.getField()
.then(function(data) {
//console.log(data);
expect(1).toEqual(100);
});
}));
});
If I run the code expect(1).toEqual(100); outside the service, the result is Error, but if I write the same code expect(1).toEqual(100); inside the service, the result is Success, which makes me think that the validator is not entering the service.
Whats wrong?
EDIT 1:
Hello Asta, I think ur idea is very good and i'm trying to implement it. I have an error in my code and i don't know how do debugging:
defer = $q.defer();
spyOn(questionApiService, 'getField').andReturn(defer.promise);
defer.resolve(data);
expect(data.nextQ).toEqual(1);
My unit testing always fails. If promise is successful, the "data" object must have nextQ attribute.
EDIT 2:
Hi Asta, your code is amazing. I'm trying to execute your code in my system and still with error. The ut fails:
Error: Unexpected request: GET http://mi.url.com/api/thefield No more request expected
Do u know what's wrong? Clarify that the code works fine on my application but ut is the problem.
Question Api Service code:
angular.module('myApp.services')
.factory('questionApiService', function($http, $q) {
var myService = {
getField: function() {
var defer = $q.defer();
$http.get('http://mi.url.com/api/thefield')
.success( function(data) {
defer.resolve(data);
})
.error( function(data) {
defer.reject(data);
});
return defer.promise;
};
return myService;
});
Your test:
describe('myApp', function () {
beforeEach(function () {
module('myApp');
});
describe('questionApiService', function () {
it('should check the existence of get field question service', inject(function($rootScope, questionApiService) {
var response = null;
var promise = questionApiService.getField();
promise.then(function(data) {
response = data;
});
$rootScope.$apply();
var expectedResponse = { "nextQ": 1 };
console.log(response);
//expect(JSON.parse(response.nextQ)).toEqual(expectedResponse.nextQ);
}));
});
});
I think you just need to move your expectation outside the then and do a $rootScope.$apply().
it('should check the existence of the get field question service', inject(function($rootScope, questionApiService) {
response = null;
promise = questionApiService.getField()
promise.then(function(data) {
response = data;
});
$rootScope.$apply();
expectedResponse = { "nextQ": "value" }
expect(JSON.parse(response)).toEqual(expectedResponse);
}));
I created a jsFiddle you can use to play around with. It sets up a service that returns JSON via a promise which I used to test http://jsfiddle.net/neridum/9uumwfzc/
Alternatively if you want to test this service from another service you can mock it out using spies. Here you would mock the response as a promise and then resolve it
defer = $q.defer();
spyOn(questionApiService, 'getField').andReturn(defer.promise);
defer.resolve(data);
expect(data).toEqual('100');

How to mock an angular $http call and return a promise object that behaves like $http

Is there a way to return an HttpPromise (or something similar) to mimic a call to $http? I want to set a global variable that indicates whether the real HTTP request is made or whether a fake HttpPromise object is returned with fake data.
For example, I have a service that is similar to this:
angular
.module('myservice')
.factory('MyService', ['$http', function($http) {
return {
get : function(itemId) {
if (isInTestingMode) {
// return a promise obj that returns success and fake data
}
return $http.get("/myapp/items/" + itemId);
}
};
} ]);
And in my controller, I have a call to the aforementioned service that looks similar to this:
// Somewhere in my controller
MyService.get($scope.itemId)
.success(function(data) {
$scope.item = data;
})
.error(function(data, status, headers, config) {
$scope.notFound = true;
});
I'm trying to not change the controller code; I want the success and error chaining to still work when in my "isInTestMode".
Is it possible to fake an HttpPromise in the way that I described in the service?
Below is a revised edition of the "MyService" above (a snippet) containing a success and error on the promise object. But, how do I execute the success method?
return {
get : function(itemId) {
if (isInTestingMode) {
var promise = $.defer().promise;
// Mimicking $http.get's success
promise.success = function(fn) {
promise.then(function() {
fn({ itemId : "123", name : "ItemName"}, 200, {}, {});
});
return promise;
};
// Mimicking $http.get's error
promise.error = function(fn) {
promise.then(null, function(response) {
fn("Error", 404, {}, {});
});
return promise;
};
return promise;
}
return $http.get("/myapp/items/" + itemId);
}
}
Just use the deferred method of the $qservice
var fakeHttpCall = function(isSuccessful) {
var deferred = $q.defer()
if (isSuccessful === true) {
deferred.resolve("Successfully resolved the fake $http call")
}
else {
deferred.reject("Oh no! Something went terribly wrong in your fake $http call")
}
return deferred.promise
}
And then you can call your function like an $http promise (you have to customize whatever you want to put inside of it, of course).
fakeHttpCall(true).then(
function (data) {
// success callback
console.log(data)
},
function (err) {
// error callback
console.log(err)
})
I found that this post is similar to what I was asking.
However, I wanted a way to mock my service call so that fake data could be returned instead of issuing a true HTTP request call. The best way to handle this situation, for me, is to use angular's $httpBackend service. For example, to bypass a GET request to my "items" resource BUT to not bypass GETs of my partials/templates I would do something like this:
angular
.module('myApp', ['ngMockE2E'])
.run(['$httpBackend', function($httpBackend) {
$httpBackend
.whenGET(/^partials\/.+/)
.passThrough();
$httpBackend
.whenGET(/^\/myapp\/items\/.+/)
.respond({itemId : "123", name : "ItemName"});
}]);
See this documentation for more information on $httpBackend.
I finally found a way using jasmin. $httpBackend was no option for me, as there were also non-$http-methods I needed mock on the same service. I also think that the controller test needing to specify the url is not perfect as imho the controller and its test should not need to know about it.
Here is how it works:
beforeEach(inject(function ($controller, $rootScope, $q) {
scope = $rootScope.$new();
mockSvc = {
someFn: function () {
},
someHttpFn: function () {
}
};
// use jasmin to fake $http promise response
spyOn(mockSvc, 'someHttpFn').and.callFake(function () {
return {
success: function (callback) {
callback({
// some fake response
});
},
then: function(callback) {
callback({
// some fake response, you probably would want that to be
// the same as for success
});
},
error: function(callback){
callback({
// some fake response
});
}
}
});
MyCtrl = $controller('MyCtrl', {
$scope: scope,
MyActualSvc: mockSvc
});
}));
You can implement your FakeHttp class:
var FakeHttp = function (promise) {
this.promise = promise;
this.onSuccess = function(){};
this.onError = function(){};
this.premise.then(this.onSuccess, this.onError);
};
FakeHttp.prototype.success = function (callback) {
this.onSuccess = callback;
/**You need this to avoid calling previous tasks**/
this.promise.$$state.pending = null;
this.promise.then(this.onSucess, this.onError);
return this;
};
FakeHttp.prototype.error = function (callback) {
this.onError = callback;
/**You need this to avoid calling previous tasks**/
this.promise.$$state.pending = null;
this.promise.then(this.onSuccess, this.onError);
return this;
};
Then in your code, you would return a new fakeHttp out of the promise.
if(testingMode){
return new FakeHttp(promise);
};
The promise must be asynchronous, otherwise it won't work. For that you can use $timeout.
easy peasy!
You can do it using angular-mocks-async like so:
var app = ng.module( 'mockApp', [
'ngMockE2E',
'ngMockE2EAsync'
]);
app.run( [ '$httpBackend', '$q', function( $httpBackend, $q ) {
$httpBackend.whenAsync(
'GET',
new RegExp( 'http://api.example.com/user/.+$' )
).respond( function( method, url, data, config ) {
var re = /.*\/user\/(\w+)/;
var userId = parseInt(url.replace(re, '$1'), 10);
var response = $q.defer();
setTimeout( function() {
var data = {
userId: userId
};
response.resolve( [ 200, "mock response", data ] );
}, 1000 );
return response.promise;
});
}]);

How to cancel $resource requests

I'm trying to figure out how to use the timeout property of a $resource to dynamically cancel pending requests. Ideally, I'd like to just be able to cancel requests with certain attributes (based on the params sent), but it seems this may not be possible. In the meantime, I'm just trying to cancel all pending requests, and then resetting the timeout promise to allow new requests.
The issue seems to be that the $resource configuration only allows a single, static promise for the timeout value. It makes sense how I could do this if I was making individual $http calls, since I could just pass in new promises for the timeout, but how can this work for a $resource? I have set up an example plunker here: http://plnkr.co/edit/PP2tqDYXh1NAOU3yqCwP?p=preview
Here's my controller code:
app.controller('MainCtrl', function($scope, $timeout, $q, $resource) {
$scope.canceller = $q.defer();
$scope.pending = 0;
$scope.actions = [];
var API = $resource(
'index.html', {}, {
get: {
method: 'GET',
timeout: $scope.canceller.promise
}
}
)
$scope.fetchData = function() {
if ($scope.pending) {
$scope.abortPending();
}
$scope.pending = 1;
$scope.actions.push('request');
API.get({}, function() {
$scope.actions.push('completed');
$scope.pending = 0;
}, function() {
$scope.actions.push('aborted');
});
}
$scope.abortPending = function() {
$scope.canceller.resolve();
$scope.canceller = $q.defer();
}
});
Right now, the canceller works when there is a pending request, but I don't seem to be able to reset it - once one request is aborted, all future requests will be aborted as well.
I'm sure I'm missing something, since being able to cancel pending requests seems like a pretty crucial feature of most web applications (at least that I've built).
Thanks
Answer by Gecko IT works for me, but I had to make some modifications in order to:
Enable resource ajax call to be canceled multiple times without need to recreate resource
Make resource backward compatible - This means that there is no need to change any application (Controller) code except resource factory
Make code JSLint compliant
This is complete service factory implementation (you just need to put proper module name):
'use strict';
/**
* ResourceFactory creates cancelable resources.
* Work based on: https://stackoverflow.com/a/25448672/1677187
* which is based on: https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/
*/
/* global array */
angular.module('module_name').factory('ResourceFactory', ['$q', '$resource',
function($q, $resource) {
function abortablePromiseWrap(promise, deferred, outstanding) {
promise.then(function() {
deferred.resolve.apply(deferred, arguments);
});
promise.catch(function() {
deferred.reject.apply(deferred, arguments);
});
/**
* Remove from the outstanding array
* on abort when deferred is rejected
* and/or promise is resolved/rejected.
*/
deferred.promise.finally(function() {
array.remove(outstanding, deferred);
});
outstanding.push(deferred);
}
function createResource(url, options, actions) {
var resource;
var outstanding = [];
actions = actions || {};
Object.keys(actions).forEach(function(action) {
var canceller = $q.defer();
actions[action].timeout = canceller.promise;
actions[action].Canceller = canceller;
});
resource = $resource(url, options, actions);
Object.keys(actions).forEach(function(action) {
var method = resource[action];
resource[action] = function() {
var deferred = $q.defer(),
promise = method.apply(null, arguments).$promise;
abortablePromiseWrap(promise, deferred, outstanding);
return {
$promise: deferred.promise,
abort: function() {
deferred.reject('Aborted');
},
cancel: function() {
actions[action].Canceller.resolve('Call cancelled');
// Recreate canceler so that request can be executed again
var canceller = $q.defer();
actions[action].timeout = canceller.promise;
actions[action].Canceller = canceller;
}
};
};
});
/**
* Abort all the outstanding requests on
* this $resource. Calls promise.reject() on outstanding [].
*/
resource.abortAll = function() {
for (var i = 0; i < outstanding.length; i++) {
outstanding[i].reject('Aborted all');
}
outstanding = [];
};
return resource;
}
return {
createResource: function (url, options, actions) {
return createResource(url, options, actions);
}
};
}
]);
Usage is the same as in Gecko IT example. Service factory:
'use strict';
angular.module('module_name').factory('YourResourceServiceName', ['ResourceFactory', function(ResourceFactory) {
return ResourceFactory.createResource('some/api/path/:id', { id: '#id' }, {
create: {
method: 'POST'
},
update: {
method: 'PUT'
}
});
}]);
Usage in controller (backward compatible):
var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {
// Successfully obtained data
}, function error(httpResponse) {
if (httpResponse.status === 0 && httpResponse.data === null) {
// Request has been canceled
} else {
// Server error
}
});
result.cancel(); // Cancels XHR request
Alternative approach:
var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {
// Successfully obtained data
}).catch(function (httpResponse) {
if (httpResponse.status === 0 && httpResponse.data === null) {
// Request has been canceled
} else {
// Server error
}
});
result.cancel(); // Cancels XHR request
Further improvements:
I don't like checking if request has been canceled. Better approach would be to attach attribute httpResponse.isCanceled when request is canceled, and similar for aborting.
(for Angular 1.2.28+)Hello All , I just wanted to make that easy to understand , how I handled that issue is as follows :
Here I declare timeout parameter
$scope.stopRequestGetAllQuestions=$q.defer();
then in I use it as follows
return $resource(urlToGet, {}, {get:{ timeout: stopRequestGetAllQuestions.promise }});
and if I want to stop previous $resource calls I just resolve this stopRequestGetAllQuestions object that is all.
stopRequestGetAllQuestions.resolve();
but if I want to stop previous ones and start a new one $resource call then I do this after stopRequestGetAllQuestions.resolve();:
stopRequestGetAllQuestions = $q.defer();
There are quite a lot of examples currently out there.
The following two I've found quite informative:
THis one shows an example how to deal with both $resource and $http requests:
https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/
and
This one is simpler and is only for $http:
http://odetocode.com/blogs/scott/archive/2014/04/24/canceling-http-requests-in-angularjs.aspx
Hi I made a custom handler based on https://developer.rackspace.com/blog/...
.factory('ResourceFactory', ["$q", "$resource", function($q, $resource) {
function createResource(url, options, actions) {
var actions = actions || {},
resource,
outstanding = [];
Object.keys(actions).forEach(function (action) {
console.log(actions[action]);
var canceller = $q.defer();
actions[action].timeout = canceller.promise;
actions[action].Canceller = canceller;
});
resource = $resource(url, options, actions);
Object.keys(actions).forEach(function (action) {
var method = resource[action];
resource[action] = function () {
var deferred = $q.defer(),
promise = method.apply(null, arguments).$promise;
abortablePromiseWrap(promise, deferred, outstanding);
return {
promise: deferred.promise,
abort: function () {
deferred.reject('Aborted');
},
cancel: function () {
console.log(actions[action]);
actions[action].Canceller.resolve("Call cancelled");
}
};
};
});
/**
* Abort all the outstanding requests on
* this $resource. Calls promise.reject() on outstanding [].
*/
resource.abortAll = function () {
for (var i = 0; i < outstanding.length; i++) {
outstanding[i].reject('Aborted all');
}
outstanding = [];
};
return resource;
}
return {
createResource: function (url, options, actions) {
return createResource(url, options, actions);
}
}
}])
function abortablePromiseWrap(promise, deferred, outstanding) {
promise.then(function () {
deferred.resolve.apply(deferred, arguments);
});
promise.catch(function () {
deferred.reject.apply(deferred, arguments);
});
/**
* Remove from the outstanding array
* on abort when deferred is rejected
* and/or promise is resolved/rejected.
*/
deferred.promise.finally(function () {
array.remove(outstanding, deferred);
});
outstanding.push(deferred);
}
//Usage SERVICE
factory("ServiceFactory", ["apiBasePath", "$resource", "ResourceFactory", function (apiBasePath, $resource, QiteResourceFactory) {
return ResourceFactory.createResource(apiBasePath + "service/:id", { id: '#id' }, null);
}])
//Usage Controller
var result = ServiceFactory.get();
console.log(result);
result.promise.then(function (data) {
$scope.services = data;
}).catch(function (a) {
console.log("catch", a);
})
//Actually cancels xhr request
result.cancel();
One solution is to re-craete the resource every time you need it.
// for canceling an ajax request
$scope.canceler = $q.defer();
// create a resource
// (we have to re-craete it every time because this is the only
// way to renew the promise)
function getAPI(promise) {
return $resource(
'index.html', {}, {
get: {
method: 'GET',
timeout: promise
}
}
);
}
$scope.fetchData = function() {
// abort previous requests if they are still running
$scope.canceler.resolve();
// create a new canceler
$scope.canceler = $q.defer();
// instead of using "API.get" we use "getAPI().get"
getAPI( $scope.canceler.promise ).get({}, function() {
$scope.actions.push('completed');
$scope.pending = 0;
}, function() {
$scope.actions.push('aborted');
});
}
In our attempt to solve this task we got to the following solution:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Cancel resource</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-resource.js"></script>
<script>
angular.module("app", ["ngResource"]).
factory(
"services",
["$resource", function($resource)
{
function resolveAction(resolve)
{
if (this.params)
{
this.timeout = this.params.timeout;
this.params.timeout = null;
}
this.then = null;
resolve(this);
}
return $resource(
"http://md5.jsontest.com/",
{},
{
MD5:
{
method: "GET",
params: { text: null },
then: resolveAction
},
});
}]).
controller(
"Test",
["services", "$q", "$timeout", function(services, $q, $timeout)
{
this.value = "Sample text";
this.requestTimeout = 100;
this.call = function()
{
var self = this;
self.result = services.MD5(
{
text: self.value,
timeout: $q(function(resolve)
{
$timeout(resolve, self.requestTimeout);
})
});
}
}]);
</script>
</head>
<body ng-app="app" ng-controller="Test as test">
<label>Text: <input type="text" ng-model="test.value" /></label><br/>
<label>Timeout: <input type="text" ng-model="test.requestTimeout" /></label><br/>
<input type="button" value="call" ng-click="test.call()"/>
<div ng-bind="test.result.md5"></div>
</body>
</html>
How it works
$resource merges action definition, request params and data to build a config parameter for an $http request.
a config parameter passed into an $http request is treated as a promise like object, so it may contain then function to initialize config.
action's then function may pass timeout promise from params into the config.
Please look at "Cancel Angularjs resource request" for details.

Resources