Prior to adding a promise, this code/spec was successfully passing
// Code
function getAvailability() {
$.ajax({
type:"POST",
url: "/get-availability",
dataType:"json",
contentType:"application/json",
data: params,
success: function() {
// ...
},
error: function() {
console.log(">> in error")
catalogDOM.updateAvailabilityForItem(0)
}
})
}
// Spec
describe("if ajax fails", function() {
beforeEach(function() {
ajaxSpy = spyOn($, "ajax")
ajaxSpy.and.callFake(function(e) {
e.error()
})
spyOn(catalogDOM, "updateAvailabilityForItem")
getAvailability()
})
it("should call updateAvailabilityForItem with 0", function() {
expect(catalogDOM.updateAvailabilityForItem).toHaveBeenCalledWith(0)
}
})
// Console output
>> in error
But then I made the ajax fire after another async function ran. I thought I spied on things correctly, and in fact, the console.log continues to indicate that the code is running. In fact, if I mocked a return value for like so spyOn(catalogDOM, "updateAvailabilityForItem").and.returnValue("works") and then in the code wrote: console.log(catalogDOM.updateAvailabilityForItem(0)), then the log does output "works"!
Yet the spec fails. Why?
// Code
function getAvailability() {
//////// START NEW LINE
utilityOrders.checkElement('#overlay').then((selector) => {
//////// END NEW LINE
$.ajax({
type:"POST",
url: "/get-availability",
dataType:"json",
contentType:"application/json",
data: params,
success: function() {
// ...
},
error: function() {
console.log(">> in error")
catalogDOM.updateAvailabilityForItem(0)
}
})
})
}
// Spec
describe("if ajax fails", function() {
beforeEach(function() {
//////// START NEW LINE
resolved_promise = new Promise(function(resolve, reject) { resolve() })
spyOn(utilityOrders,"checkElement").and.returnValue(resolved_promise)
//////// END NEW LINE
ajaxSpy = spyOn($, "ajax")
ajaxSpy.and.callFake(function(e) {
e.error()
})
spyOn(catalogDOM, "updateAvailabilityForItem")
getAvailability()
})
it("should call updateAvailabilityForItem with 0", function() {
expect(catalogDOM.updateAvailabilityForItem).toHaveBeenCalledWith(0)
}
})
// Console output
>> in error
Try making the following changes:
// Code
// !! add async here so we have control to wait until it goes inside of the .then before continuing !!
async function getAvailability() {
//////// START NEW LINE
utilityOrders.checkElement('#overlay').then((selector) => {
//////// END NEW LINE
$.ajax({
type:"POST",
url: "/get-availability",
dataType:"json",
contentType:"application/json",
data: params,
success: function() {
// ...
},
error: function() {
console.log(">> in error")
catalogDOM.updateAvailabilityForItem(0)
}
})
})
}
// Spec
describe("if ajax fails", function() {
// !! add async and done argument to call when we are done with this function !!
beforeEach(async function(done) {
//////// START NEW LINE
resolved_promise = new Promise(function(resolve, reject) { resolve() })
spyOn(utilityOrders,"checkElement").and.returnValue(resolved_promise)
//////// END NEW LINE
ajaxSpy = spyOn($, "ajax")
ajaxSpy.and.callFake(function(e) {
e.error()
})
spyOn(catalogDOM, "updateAvailabilityForItem")
// await getAvailability so the promise is resolved before proceeding (then is called)
await getAvailability();
// call done to let Jasmine know you're done with the test
done();
})
it("should call updateAvailabilityForItem with 0", function() {
expect(catalogDOM.updateAvailabilityForItem).toHaveBeenCalledWith(0)
}
The above modifications should hopefully fix it for you. Dealing with promises and tests is difficult because sometimes in the test you have to tell Jasmine that the promises that were created in the code that is being tested, wait for them to complete before doing assertions.
Related
Here is my code:
// promise chain being run onClick
function clickSequence(paramObj) {
checkForDuplicateNums(paramObj)
.then(() => {
checkForDuplicateEmails(paramObj);
})
.then(() => {
insertReservationForm(paramObj);
})
.then(() => {
sendEmail(paramObj);
})
.then(() => {
updateResBtnCounter(paramObj);
})
.then((pageUrl) => {
goToPage(pageUrl);
});
.catch((err) => {
displayErrorMsg(err);
})
}
// display sweet alert 2 modal
function displayErrorMsg(err) {
Swal.fire({
title: 'error',
icon: 'error',
text: `${err}`
});
}
// checking for duplicate numbers in DB
function checkForDuplicateNums(paramObj) {
console.log('checkforduplicatenums');
const { campaignId, numberInputValue, errorMsg } = paramObj;
functionName = "checkForDuplicatesNums";
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "./includes/ajax_general_utilities.php",
data: {
functionName,
campaignId,
numberInputValue,
},
cache: false,
success: function(resp) {
if (resp === 'true') {
reject(new Error(`${errorMsg}`));
} else {
resolve();
}
},
error: function(err) {
console.error(err);
}
}); // close ajax
}); // close promise
}
// checking for duplicate emails in DB
function checkForDuplicateEmails(paramObj) {
const { emailInputValue, campaignId, errorMsg } = paramObj;
functionName = "checkForDuplicateEmails";
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "./includes/ajax_general_utilities.php",
data: {
functionName,
campaignId,
emailInputValue
},
cache: false,
success: function (resp) {
if (resp === 'true') {
reject(new Error(`${errorMsg}`));
} else {
resolve();
}
},
error: function (err) {
console.error(err);
}
}); // close ajax
}); // close promise
}
I'm trying to chain a bunch of functions together where I wrapped an ajax call in a promise. checkForDuplicateEmails and checkForDuplicateNums check the database for duplicate values and if they find a duplicate, they call reject(). I'm then passing an error message to a function name displayErrorMessage in the catch block that displays a sweet alert 2 modal.
I was testing each one individually to make sure they're working correctly but I'm running into an issue. I hardcoded values that I know are already in the database to make sure reject() is called. When I comment out all except the first function checkForDuplicateNums the error is thrown and it is correctly passed to displayErrorMsg and a modal pops up. If I uncomment out the first .then() call and change it so checkForDuplicateNums is resolved, instead of checkForDuplicateEmails displaying the error message correctly as a modal, I only get an error in the console that looks like:
Uncaught (in promise) Error: <then it displays error message only in console>
at Object.success (<page url>:231)
at c (jquery-3.5.1.min.js:2)
at Object.fireWith [as resolveWith] (jquery-3.5.1.min.js:2)
at l (jquery-3.5.1.min.js:2)
at XMLHttpRequest.<anonymous> (jquery-3.5.1.min.js:2)
I'm not sure why the modal will pop up only when the first function is called and not after that, I assume I'm not using promises quite right, but I can't seem to find an answer specific to my situation from searching around online.
I want to wait constructor() which has async method handled by Promise
What I want to do is waiting two async method in constructor, and then wait constructor itself.
However my code shows the error Uncaught (in promise) ReferenceError: resolve is not defined
What is the best practices for this purpose??
class MyClass{
constructor(){
var p1 = new Promise(function(resolve, reject){
$.ajax({
type: "GET",
url: api1
success: function(response) {
console.log(response);
resolve();
}
});
});
var p2 = new Promise(function(resolve, reject){
$.ajax({
type: "GET",
url: api2
success: function(response) {
console.log(response);
resolve();
}
});
});
Promise.all([p1,p2]).then(function(value){
console.log("finish");
resolve(); // this shows error.
});
}
}
$(function() {
var temp = new Promise(function(resolve,reject){
var myClass = new MyClass();
});
temp.then(function (value) {
console.log("finish all");
});
}
A constructor will always run synchronously. In addition to that, you don't want the explicit Promise construction anti-pattern, so you wouldn't want to call resolve inside a Promise.all anyway. The Promise.all([p1,p2]) will be a Promise that resolves when both p1 and p2 have resolved, so assign that a property to the instance, and call .then on that property. Change
Promise.all([p1,p2]).then(function(value){
// ...
});
to
this.apiPromises = Promise.all([p1,p2]);
and, when constructing the instance:
$(function() {
const myInstance = new MyClass();
myInstance.apiPromises
.then(function (value) {
console.log("finish all");
})
.catch((error) => {
// handle errors
});
});
(If you aren't calling either of the resolves of with anything in p1 or p2, best to delete the value parameter, since it holds no useful information)
$.ajax returns a thenable already, so you don't need to call new Promise when creating the p1 and p2s:
var p1 = $.ajax({
type: "GET",
url: api1, // remember to put commas after values in an object to avoid a SyntaxError
success: function(response) {
console.log(response);
}
});
var p2 = $.ajax({
type: "GET",
url: api2, // remember to put commas after values in an object to avoid a SyntaxError
success: function(response) {
console.log(response);
}
});
(using the above will also properly reject the Promise.all when an error occurs)
I'm having some trouble. I'm trying to execute my ajax function 1 by 1, not all at the same time. I'm using promise but I have no more idea on how to achieve it. Here is my code :
function run_action(action){
if(action == "login"){
return $.ajax({
url: "login.php",
type: "post",
data: {password: password},
beforeSend: function() {
console.log('beforeSend login');
},
success: function (response) {
console.log('Success Login');
},
error: function (request, error) {
console.log('Error Login');
},
})
}
if(action == "register"){
return $.ajax({
url: "register.php",
type: "post",
data: {password: password},
beforeSend: function() {
console.log('beforeSend register');
},
success: function (response) {
console.log('Success Register');
},
error: function (request, error) {
console.log('Error Register');
},
})
}
}
var actions = ['register', 'login'];
services.forEach(checkActions);
function checkActions(item, index) {
if (document.getElementById(item).checked) {
var promise = run_action(item);
promise.success(function (data) {
console.log('Run after');
});
console.log('Run first')
}
}
In this case login and register are both launched at the same time, login doesn't wait for register to finish so he can start processing.
In case you can't properly wait for checkActions from the outside, you could maintain a task queue for that:
let queue = Promise.resolve();
function checkActions(item, index) {
queue = queue
.then(() => run_action(item))
.then(() => {
console.log("Next item was processed", item);
// Your code here
});
// Synchronous code here - This won't execute in order!
}
Currently your code runs through the forEach loop with each action and invokes checkActions with that action, thus firing the request. Array.prototype.forEach executes synchronously (without any kind of check to the promises returned by $.ajax). The following would wait for 'register' to finish before firing 'login':
function checkActions(item) {
if (document.getElementById(item).checked) {
return run_action(item);
}
}
checkActions('register')
.then(data => {
return checkActions('login');
});
I'm not super familiar with jQuery's promise structure, so I used .then, but I believe you can use .success without issue as well.
Unrelated comment, your run_actions function should really be split into two functions (login and register) since they are completely unrelated aside from the fact that they are making requests.
First- its not a good practice to trust a order-based function (AKA - run them by the array order), run your functions according to logic.
For example: if the first function was failed - you dont want to run the next functions in the array.
If you consist to run the functions in array - you can use an async
async function runActions( actionsList ) {
for(const action of actionsList) {
await run_action( action );
}
};
In general - we use the then method to run anther function when specific promise is done. Like so:
promise.then( onSuccess => {
// try to log in
}, onFail => {
// tell the user the signup was failed
});
BTW - you can use the native fetch instade of jQuery ajax, and get simple to use, promise-based way to communicate with your sever.
Like so:
fetch("login.php" , {
method: 'POST', // or 'PUT'
body: {password: password}, // data can be `string` or {object}!
headers:{
'Content-Type': 'application/json'
}
}).then( ... )
asynFn(url, callback)
This function takes a url and fires some xhr requests, then uses callback(result) to send back processed result. How should I test it?
(I've run the asynFn directly in Chrome and it worked fine.)
I tried to use jasmine-ajax to stub the request, but the expect did't work.
describe('a test', function() {
var callback
beforeAll(function() {
jasmine.Ajax.install()
jasmine.Ajax.stubRequest('fake/path1').andReturn({
status: 200,
contentType: 'text/plain',
responseText: 'yay'
})
jasmine.Ajax.stubRequest('fake/path2').andReturn({
status: 200,
contentType: 'text/plain',
responseText: 'yay2'
})
// ...
})
afterAll(function() {
jasmine.Ajax.uninstall()
})
beforeEach(function() {
callback = jasmine.createSpy('sendResponse')
})
it('a spec', function() {
asynFn('input string', callback)
expect(jasmine.Ajax.requests.mostRecent().url).toBe('fake/path2')
expect(callback).toHaveBeenCalled() // faild
})
})
What am I missing here?
The problem is that asynFn is asynchronous and the callback y called after the expect sentence is executed.
Think you test like a history.
Subject Under Testing (describe)
When asynFn is executed (beforeEach)
Then: A method or callback should be called (it)
Change your code to:
beforeEach(function() {
callback = jasmine.createSpy('sendResponse');
asynFn('input string', callback);
});
afterEach(function() {
callback = null;
});
it('a spec', function() {
expect(jasmine.Ajax.requests.mostRecent().url).toBe('fake/path2')
expect(callback).toHaveBeenCalled() // faild
})
if The first doesn't work, try this:
beforeEach(function(done) {
callback = jasmine.createSpy('sendResponse');
asynFn('input string', function() {
callback();
done(); //<-- This tells jasmine tha async beforeEach is finished
});
});
When using JQuery.Deferred is it OK to invoke reject() directly? Without having invoked a async function?
Perhaps I want some kind of test in the beginning of my async function. If the test fails I want to reject immediately. See the first if block below.
function doSomethingAsync() {
//Test if the ajax call should be invoked
var testFailed = true;
var dfd = $.Deferred();
//Check if test failed
if (testFailed) {
var asyncResult = {
success: false,
data: 'test failed'
};
//Is this OK usage of reject on the same thread?
dfd.reject(asyncResult);
return dfd.promise();
}
$.get('/api/testapi/get').done(function (data) {
var asyncResult = {
success: true,
data: data
};
dfd.resolve(asyncResult);
}).fail(function (err) {
var asyncResult = {
success: false,
data: err
};
dfd.reject(asyncResult);
});
return dfd.promise();
}
When using JQuery.Deferred is it OK to invoke reject() directly? Without having invoked a async function?
Yes, it's totally OK to return an already rejected promise, and to reject deferreds immediately. You only might need to verify that your callbacks don't rely on asynchronous resolution, which jQuery does not guarantee (in contrast to A+ implementations).
Notice that in your code you should use then instead of manually resolving the deferred:
function doSomethingAsync() {
var testFailed = /* Test if the ajax call should be invoked */;
var dfd = testFailed
? $.Deferred().reject('test failed')
: $.get('/api/testapi/get');
return dfd.then(function (data) {
return {
success: true,
data: data
};
}, function (err) {
return {
success: false,
data: err
};
});
}
You can do it quickly, as your function return a Promise object:
return Promise.reject('test failed');