Now Angular 1.5.4 finally allows you to track progress event on $http provider but for some reason I keep getting the $rootScope as a response instead of an actual progress (I'm using it for uploads) information. Because of lack of examples I found some tests in the Angular repo and followed that but to no success.
restClientInstance.post = function (requestParams) {
var postParams = {
method: "POST",
url: API_URL + requestParams.url,
headers: requestParams.headers,
data: requestParams.data,
eventHandlers: {
progress: function (c) {
console.log(c);
}
},
uploadEventHandlers: {
progress: function (e) {
console.log(e);
}
}
};
var promise = $http(postParams)
$rootScope.$apply();
return promise;
};
In both cases it consoles $rootScope rather than the lengthComputable
In AngularJS v1.5.7 works fine. If you have the chance I recommend upgrade!
...//formData = new FormData(); etc...
var postParams = {
method: 'POST',
url: yourURLWS,
transformRequest: angular.identity,
uploadEventHandlers: {
progress: function (e) {
if (e.lengthComputable) {
$scope.progressBar = (e.loaded / e.total) * 100;
$scope.progressCounter = $scope.progressBar;
}
}
},
data: formData,
headers: {'Content-Type': undefined }
};
var sendPost = $http(postParams); //etc...
in HTML you have:
<progress id="progress" max="100" value="{{progressBar}}"></progress>{{progressCounter}}%
Result:
progress result
The feature is broken for now: https://github.com/angular/angular.js/issues/14436
Well I ended up doing something like this and just handle it myself as the XHR events added to $http dont work for me.
var xhttp = new XMLHttpRequest();
var promise = $q.defer();
xhttp.upload.addEventListener("progress",function (e) {
promise.notify(e);
});
xhttp.upload.addEventListener("load",function (e) {
promise.resolve(e);
});
xhttp.upload.addEventListener("error",function (e) {
promise.reject(e);
});
xhttp.open("post",API_URL + requestParams.url,true);
xhttp.send(requestParams.data);
return promise.promise;
note - I have not worked with NG 1.5.4, the example below is for leveraging existing pre 1.5.4 APIs
The notify(event) API is part of the deferred object when you call $q.defer(). I'm not sure what a practical implementation of this would be in terms of a typical get/put/post call via $http. But if you want to see it in action you can do something like this:
some service API
var mockRqst = function(){
var d = $q.defer()
var crnt = 0
$off = $interval( function(){
d.notify( crnt )
crnt += 5
if (crnt >= 100)
{
$interval.cancel( $off ) //cancel the interval callback
d.resolve( "complete" )
}
}
return d.promise
}
using the notification
someService.mockRqst()
.then( thenCallback, catchCallback, function( update ){
console.log("update", update)
})
codepen - http://codepen.io/jusopi/pen/eZMjrK?editors=1010
Again, I must stress that I'm not entirely sure how you can tie this into an actual external http call.
As seen in the docs here, the third parameter in a promise is a notify function.
notify(value) - provides updates on the status of the promise's execution. This may be called multiple times before the promise is either resolved or rejected.
It can be used like this:
$http(requestData)
.then(
function success() {
console.log('success');
},
function error() {
console.log('error');
},
function notify() {
console.log('notified');
}
);
Related
I am trying to write unit test to cover method A complete() block. I am able to mock ajax request using Deferred. But Deferred does not support complete() so I am getting below error
TypeError: _this.methodB(...).complete is not a function. Please help me to cover methodB(..).complete() block.
methodB: function(xURL, container) {
var _this = this;
return $.ajax({
type: 'GET',
url: xURL,
async: false,
dataType: 'html',
timeout: _this.ajaxTimeOut
})
.fail(function(resp) {
_this.doSomethingOnFail();
})
.done(function(resp, textStatus, jqXHR) {
if (jqXHR.status === 200 && resp !== '') {
_this.doSomethingOnDone();
}
});
},
methodA: function(e) {
var _this = this,
_this.methodB(_this.refineURL, _this.$el)
.complete(function(resp) {
**if (resp.responseText !== undefined &&
resp.responseText.indexOf("PRICE_DATA_AVLBL = 'false'") > -1) {
var params1 = _this._getFilterURLParameters(_this.refineURL);
var params2 = _this._getFilterURLParameters(_this.SUCC_URL);
if (params1.lowerbound !== params2.lowerbound ||
$(e.currentTarget).hasClass('js-txt-min')) {
$txtMin.addClass('border-danger');
} else {
$txtMin.val(params2.lowerbound);
}
} else {
_this._pageSubmit();
}**
});
}
Unit Test Code :
it('validate ajax complete', function ajaxComplete(){
spyOn($, 'ajax').and.callFake( function fake() {
XMLHttpRequest = jasmine.createSpy('XMLHttpRequest');
var jqXHR = new XMLHttpRequest();
jqXHR.status = 200;
var dea = new $.Deferred();
dea.resolve('{property:value}',' ', jqXHR);
return dea;
});
f.methodA();
});
Mock dependencies
It is important to remember that when testing a function, you mock dependencies of that function. You don't want to actually invoke those dependent functions in your tests, because you aren't testing those functions. You should be testing those functions elsewhere, and mocking their dependencies, etc.
Your code
With this in mind when testing methodA, you shouldn't care that methodB makes an ajax request. All you care about is that it returns some object that has a complete function, and that you wire up a callback correctly, etc.
Tests
The following (untested) code should roughly work for you, or give you a decent starting point.
describe('.methodA()', function() {
var methodBResult;
beforeEach(function() {
methodBResult = jasmine.createSpyObj('result', ['complete']);
spyOn(f, 'methodB').and.returnValue(methodBResult);
});
it('should call .methodB()', function() {
f.refineURL = 'something for the test';
f.$el = 'something else for the test';
f.methodA();
expect(f.methodB.calls.count()).toBe(1);
expect(f.methodB).toHaveBeenCalledWith(f.refineURL, f.$el);
});
it('should register a callback on complete', function() {
f.methodA();
expect(methodBResult.complete.calls.count()).toBe(1);
expect(methodBResult.complete).toHaveBeenCalledWith(jasmine.any(Function));
});
it('should call .doSomethingOnComplete() when the callback is invoked', function() {
spyOn(f, 'doSomethingOnComplete');
f.methodA();
var callback = methodBResult.complete.calls.argsFor(1)[0];
callback();
expect(f.doSomethingOnComplete.calls.count()).toBe(1);
expect(f.doSomethingOnComplete).toHaveBeenCalledWith();
});
});
I have a web app that needs to check if the user is connected to the internet. In the implementation, the check() function promises to true if an ajax ping to a known endpoint succeeds and false if the ajax call fails in any way.
In Jasmine, I can use request.respondWith({status:400, etc}) to simulate a failure, but I can't work out how to simulate the more fundamental error of the call not being made at all.
In practice, browsers seem to 'return' a status code 0 and readyState 4 when the call could not even be made.
How should I approach this in my Jasmine tests?
This is an interesting question. Though I might not provide you a straight forward answer for it, I'd like to take a stab at it.
The code below illustrates three such examples (tests rather). See it in action
var errorFunction = function(jqXHR, status, error) {
console.log('This is errorFunction')
}
var makeAPICall = function(url) {
return $.ajax({
url: url,
fail: errorFunction,
error: errorFunction
});
}
var testFunction = function() {
makeAPICall().done(function(data, status, xh) {
if (xh.status === 0 && xh.readyState == 4) {
errorFunction();
return;
}
console.log("this is successFunction");
}).fail(errorFunction);
}
var getData = function() {
var str = 'ABCDEFGHIJ'
var returnStr = '';
for (var i = 0; i < 3000; i++) {
returnStr += str;
}
return returnStr;
}
describe('test ajax errors', function() {
it('tests a failed call due to huge payload ', function() {
var xhrObj = $.ajax({
url: 'https://jsonplaceholder.typicode.com/posts?userId=' + getData(),
async: false
});
console.log('Below is the xhr object that is breing returned via spy');
console.log(xhrObj);
spyOn(window, 'makeAPICall').and.returnValue(xhrObj);
spyOn(window, 'errorFunction').and.callThrough();
testFunction();
expect(window.errorFunction).toHaveBeenCalled();
});
it('tests a failed call due to some n/w error scenario (readyState->4, status->0)', function() {
var xhrObj = $.ajax({
url: '',
async: false
});
xhrObj.status = 0;
xhrObj.statusText = 'error';
console.log('Below is the xhr object that is breing returned via spy');
console.log(xhrObj);
spyOn(window, 'makeAPICall').and.returnValue(xhrObj);
spyOn(window, 'errorFunction').and.callThrough();
testFunction();
expect(window.errorFunction).toHaveBeenCalled();
});
it('tests a failed call (bad url pattern)', function() {
var xhrObj = $.ajax({
url: 'https://jsonplaceholder.typicode.com/postssss/1',
async: false
});
console.log('Below is the xhr object that is breing returned via spy');
console.log(xhrObj);
spyOn(window, 'makeAPICall').and.returnValue(xhrObj);
spyOn(window, 'errorFunction').and.callThrough();
testFunction();
expect(window.errorFunction).toHaveBeenCalled();
});
});
Notes:
errorFunction is the errorFunction that gets called from the done
if the readySatate === 4 && status === 0 and also all other
error/fail callbacks
makeAPICall returns the jqXHR on which done & fail callbacks are used.
getData is a utility function which generates a huge payload: used in test1
second test is about making a dummy ajax call with empty url so as to simulate readyState->4 & status->0
the third test simulates the usual bad request url pattern test.
for all the 3 scenarios, I've used a jqXHR object that I created by $.ajax({some params & async false})
setting async: false helps me to generate a dummy object and pass it over to the function via a spy
I have a JavaScript client that works in Chrome and Firefox, but fails in IE. Looking at the network trace in the IE debugger it shows that multiple of the AJAX calls have been aborted.
I've been able to get around it by setting the timeout to 0. I'd like to know if this is the correct way to handle my requests being aborted? Basically what could go wrong?
My initial thought was that I should capture and resend on error, and if multiple resubmits do not result in a completed request, finally alert the user. I'd still like to know how to do this even if the setTimeout is the proper way to address my immediate issue.
Also the application will process an excel workbook of addresses, call a web service to add some data to them and then allow the user to download the enhanced file.
This is what I have so far, first in the app.js
var requestWithFeedback = function (args) {
$(".loader").removeClass('hidden');
var oldConfig = args.config || function () { };
args.config = function (xhr) {
xhr.setRequestHeader("Authorization", "Bearer " + localStorage.token);
oldConfig(xhr);
extract: extract;
};
var deferred = m.deferred();
setTimeout(function () { // <== This solved in IE, but is this the way to handle this?
m.request(args).then(deferred.resolve, function(err){
if (err === "Invalid token!"){
m.route('/');
}
})}, 0);
$(".loader").addClass('hidden');
return deferred.promise;
}
From the model.js
app.MarkedAddresses.ProcessAddressBatch = function () {
var requestData = {
Addresses: app.MarkedAddresses.vm.addresses
}
return requestWithFeedback({
method: "POST"
, url: "API/server.ashx"
, data: requestData
, deserialize: function (value) { return value; }
})
.then(function (value) {
var responseJSON = $.parseJSON(value);
$.merge(app.MarkedAddresses.vm.results, responseJSON)
app.MarkedAddresses.vm.currentRecord(app.MarkedAddresses.vm.results.length);
app.MarkedAddresses.vm.progress(Math.max(app.MarkedAddresses.vm.progress(), ~~(app.MarkedAddresses.vm.currentRecord() / app.MarkedAddresses.vm.totalRecords() * 100)));
m.redraw(); //Force redraw for progress bar
return value;
},
function (error) { console.log(error) } // <== I thought error would show up here, but I never hit a breakpoint here.
);
}
Added loops
function process_wb(wb) {
app.MarkedAddresses.vm.results.length = 0;
$('.descending').removeClass("descending");
$('.ascending').removeClass("ascending");
app.MarkedAddresses.vm.progress(.1);
m.redraw();
var header = mapHeader(wb);
var addressJSON = to_json(wb, header);
app.MarkedAddresses.vm.totalRecords(addressJSON.length);
for (var i = 0; (i < addressJSON.length + 1) ; i += 1000) {
app.MarkedAddresses.vm.addresses = addressJSON.slice(i, Math.min(((i) + 1000), addressJSON.length));
app.MarkedAddresses.vm.response(new app.MarkedAddresses.vm.processAddressBatch());
}
}
Why isn't the error triggered in the section of the code?
It seems like I should add a deferred section here, but anything I've tried has been a syntax error.
I'm making a jquery library to use an application with the json rpc protocol but I'm stuck with a little problem.
This is the fiddle that shows the code (obviously it can't work): https://jsfiddle.net/L9qkkxLe/3/.
;(function($) {
$.lib = function(options) {
var outputHTML = [],
plugin = this;
var APIcall = function(api_method, api_params) {
request = {};
request.id = Math.floor((Math.random() * 100) + 1);
request.jsonrpc = '2.0';
request.method = api_method;
request.params = (api_params) ? api_params : [];
$.ajax({
type: "POST",
url: "http://localhost:8898/jsonrpc",
data: JSON.stringify(request),
timeout: 3000,
beforeSend: function(xhr) {
xhr.setRequestHeader('Authorization', window.btoa(options.username + ":" + options.password));
},
success: function(data) {
handleData(data, api_method);
},
error: function(jqXHR, textStatus, errorThrown) {
log("Connection time out: can't reach it. Try changing the settings.");
isConnected = "false";
},
dataType: "json"
});
}
var handleData = function(data, method) {
if (method == "getgenres") {
outputHTML = data.result.genres; //I need data.result.genres to return in getgenres function
}
}
var log = function(msg) {
if (options.debug == true) console.log(msg);
}
plugin.getgenres = function() {
APIcall("getgenres");
return outputHTML; //This is sadly empty.
}
};
}(jQuery));
var init = new $.lib();
console.log(init.getgenres());
I need that the getgenres function returns data.result.genres but actually it returns an empty array because getgenres is called for first and only after the handleData function gives to outputHTML the value that I need.
You are performing an asynchronous AJAX request, which means you can't actually get back the data immediately. There are two ways to solve your issue: making it synchronous (easy but ill advised) or using a callback (a little bit more complex but generally accepted):
In your getgenres function, you could accept one more parameter: callback
plugin.getgenres = function(callback) {
/* Dont forget APIcall already took two parameters in, so callback has to be the third in line! */
APIcall("getgenres", false, callback);
}
Now modify your APIcall function to accept your callback:
var APIcall = function(api_method, api_params, callback) { ... }
And call the callback from the successful completion call - instead of having a handler method in between wrapped in a function, you can simply pass the anonymous function. So instead of success: function(data){ handle(data); }, just use:
success: callback
The anonymous function that we will pass to it will receive as its first parameter the data you were passing to the handler. Now you can do the following:
var myGenres = [];
var init = new $.lib();
init.getgenres(function(data){
/* Now your data is actually loaded and available here. */
myGenres = data;
console.log(myGenres);
});
I would like to point out that there are many better ways to handle this, including turning this into a Constructor (More here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) instead of the strange amalgamation of functions and variables you have now, as well as using JS Promises (here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to make this easier. But the basic gist should be here.
Update (potential implementation)
Because I mentioned that this could be done in a way that I think is clearer to read and use. I do not know all use cases for this, but from the provided example I would change the code to something looking like the following. Please also note I am not an expert on jQuery plugins, so I am avoiding plugging into jQuery and just using it as an easy AJAX call.
function getAjax(){
if(!window.jQuery || !window.$) throw("jQuery is required for this plugin to function.");
this.data = [];
this.request = '';
return this;
}
getAjax.prototype = {
createRequest : function(method, parameters){
this.request = {};
this.request.id = Math.floor((Math.random() * 100) + 1);
this.request.jsonrpc = '2.0';
this.request.method = method;
this.request.params = parameters || [];
return this;
},
callRequest : function(options, callback, error){
var self = this;
// We could also `throw` here as you need to set up a request before calling it.
if(!this.request) return this;
else {
$.ajax({
// We will allow passing a type and url using the options and use sensible defaults.
type: options.type || "POST",
url: options.url || "http://localhost:8898/jsonrpc",
// Here we use the request we made earlier.
data: JSON.stringify(this.request),
timeout: options.timeout || 3000,
beforeSend: function(xhr){
xhr.setRequestHeader(
'Authorization',
window.btoa( options.username + ":" + options.password)
);
},
// We will also store all the made request in this object. That could be useful later, but it's not necessary. After that, we call the callback.
success: function(data){
var store = {request:self.request, data: data};
self.data.push(store);
// Call the callback and bind `this` to it so we can use `this` to access potentially pther data. Also, pass the results as arguments.
callback(data, self.request.id).bind(self);
},
// Error function!
error: error,
dataType: options.dataType || "json"
});
}
return this;
}
}
// Example use
new getAjax().createRequest('getgenres').callRequest({
username: 'myusername',
password: 'mypassword'
}, function(data, id){
// Success! Do with your data what you want.
console.log(data);
}, function(e){
// Error!
alert('An error has occurred: ' + e.statusText);
console.log(e);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
What I do in those occasions is this:
You are supplying a method. So put a reference to the a callback function. In this case plugin.getGenresFinalize. When handleData is called it will fire that callBack function. This way you can pass multiple methods to the api call for different types of data.
plugin.getgenres = function() {
APIcall(this.getgenresFinalize);
}
plugin.getgenresFinalize = function(data) {
console.log(data);
}
var handleData = function(data, method) {
method(data);
}
In order to check the submission of an ajax request, when a user submit a form, I implemented the following test using jasmine (1).
The test is successfully passed but looking at javascript console I get the following error 500 (Internal Server Error).
Since I don't care the server response, my questions are:
1) Should I or should I not care about this error.
2) Will it be better to fake the ajax request to avoid Internal Server Error? if yes, how in my context?
(1)
define([
'backendController'
], function (backendController) {
// some code
describe('When button handler fired', function () {
beforeEach(function () {
spyOn(backendController, 'submitRequest1').andCallThrough();
this.view = new MyView({
el: $('<div><form><input type="submit" value="Submit" /></form></div>')
});
this.view.$el.find('form').submit();
});
it('backendController.submitRequest1 should be called', function () {
expect(backendController.submitRequest1).toHaveBeenCalled();
});
});
});
(2)
// backendController object
return {
submitRequest1: function (message) {
return $.ajax({
url: 'someUrl',
type: 'POST',
dataType: 'json',
data: {
message: message
}
}).done(function () {
// some code
});
}
}
(3)
// submitForm in MyView
submitForm: function (event) {
event.preventDefault();
var formElement = event.currentTarget;
if (formElement.checkValidity && !formElement.checkValidity()) {
this.onError();
} else {
backendController. submitRequest1('some data').done(this.onSuccess).fail(this.onError);
}
}
Your test says "backendController.submitRequest1 should be called" so you only care about the method to be called. So, just don't andCallThrough() and you'll be free of troubles.
Update
Maybe you have to preventDefault() in the submit event so:
spyOn(backendController, 'submitRequest1').andCallFake(function(e) { e.preventDefault(); });
Update 2
After checking the rest of your code you have added to the question I still suggest to stop your code execution as sooner as possible. Just until the test in arriving, and this is still until backendController.submitRequest1() is called.
In this case is not enough with a simple mock in the method due the return of this method is used subsequently and your mock should respect this:
// code no tested
var fakeFunction = function(){
this.done = function(){ return this };
this.fail = function(){ return this };
return this;
}
spyOn(backendController, 'submitRequest1').andCallFake( fakeFunction );
When things start to be so complicate to test is a symptom that maybe they can be organized better.