I just wrote:
function getQtyFor(state,down) {
var promise = new RSVP.Promise( function (fulfill, reject) {
if (!down && (state=='bottle' || state=='pumped')) {
doDialog('volume')
.then ( validateVolume ,
function () { hideDialog('volume');
return getQtyFor(state,down); } )
.then ( function (q) { hideDialog('volume');
fulfill(q); },
function (e) { hideDialog('volume');
alert("Bad input "+e);
return getQtyFor(state,down); } )
} else fulfill(0);
});
return promise;
}
in which you can see two recursive calls. I'm finding that the first works as expected, and the second goes round in circles as expected, but I never get a 'fulfillment' after the second is used. In other words, I can run around in circles either by cancelling the dialog (first recursion) or by entering values that the validator doesn't like (second recursion.) In the former case, after I finally decide to hit OK, the waiting "then" will run and drop stuff in my database. But if I go round in circles by entering silly values and then finally enter a sensible value, then the waiting "then" will not run, and neither will it's rejection handler.
I kinda suspect the problem is that I'm fulfilling a different promise than the one with my handlers attached, but I still don't see why it works one way and not the other.
I guess you'll need the rest of the code to reproduce this so it's at http://quorve.com/rsvpissue/babylog.html. Here's the aforementioned waiting "then":
function stick(down) {
return function(ob) {
return getQtyFor(ob.attr('id'), down)
.then( getStickyFields(down, ob), alert ) //NEITHER REACHED AFTER FAILED VALIDATE
.then( db_insert('transitions') )
.then( function() { updateButtonAppearance(down)(ob); } , alert);
}
}
and the stuff called from the problematic recursive function:
function doDialog(log) {
var promise = new RSVP.Promise( function(fulfill, reject) {
$('#'+log).dialog({
dialogClass: "no-close",
modal: true,
buttons: {
"OK": function() { fulfill($(this)); },
"Cancel": function() { reject(); }
}
});
});
return promise;
}
function validateVolume(form) {
vol = form.find("[name=qty]").val();
if ( vol > 200 ) {
throw("vol"); }
return vol;
}
Steps to reproduce:
Use Chrome cos it uses WebSQL. (I know it's depped, but let's not change the subject.)
Go to http://quorve.com/rsvpissue/babylog.html
Click Zach's bottle (depressed button) and hit cancel in the dialog. The dialog persists until you hit OK, after which, you see a new transition bottom right representing the end of the drinking session with a quantity of 150ml. You were using the first recursive call when you were pressing cancel.
Reload the page (which resets the DB.) Click the bottle as before but push the slider to the top (no baby can drink that much) and press OK. Dismiss the nag box and the dialog reappears. Repeat if you like. Push the slider to a lowish value and press OK. Now you see the problem: the dialog disappears, so we've dropped out of the recursion, but the button and DB are not updated because the continuation is not called and neither is its rejection case.
Is this a bug in RSVP or have I misunderstood promises?
TIA, Adrian.
Sussed it! I should say:
fulfill(getQtyFor(state,down))
rather than
return getQtyFor(state,down)
for the recursive call.
Related
I have a page that has an element where if you scroll down, it loads new data.
This takes about 10 seconds.
I have written the following test:
it('Should display at least one facility in booking panel', (done) => {
function recursivelyScroll() {
cy.get(element)
.scrollTo('bottom');
cy.get(element)
.then($el => {
// If the element contains a loading class, we wait one second and scroll down again
if ($el.find(Loading).length > 0) {
setTimeout(recursivelyScroll, 1000);
} else {
// We are done waiting, no more loading is needed
// write test here
done();
}
});
}
recursivelyScroll();
});
CypressError
Timed out after 4000ms. The done() callback was never invoked!
The done() method is not called fast enough according to Cypress, but they have no documentation on how to extend the done time period. Also, there might be a better way that I'm unaware of to create this scrollbehaviour in Cypress. Is there an easy fix?
Have you tried using the recursion plugin in cypress?
It would look something like this:
cy.get(element).scrollTo('bottom');
recurse(
() => cy.contains('element', 'Loading').should(() => {}),
($quote) => $quote.length > 0 ,
{
limit: 300,
log: false,
post(){
setTimeout(recursivelyScroll, 1000);
}
},
)
The idea was taken here: https://www.youtube.com/watch?v=KHn7647xOz8
Here example how to use and install https://github.com/bahmutov/cypress-recurse
Try to switch to When assertion and declare that function outside the execution:
When('Should display at least one facility in booking panel', (done) => {
recursivelyScroll(done)
}
function recursivelyScroll(done){
//see implementation below
}
Not sure how about which "Loading" is, maybe you have to put into double quotes?
Moreover please note that cypress is asynchronous, then the scrollTo and the then code sections are executed at same time in your code, just use then after the scroll.
And I will change the setTimeout into cy wait function before executing recursion, give a try to below code:
function recursivelyScroll(done) {
cy.get(element)
.scrollTo('bottom')
.then($el => {
// If the element contains a loading class, we wait one second and scroll down again
if ($el.find(".Loading").length > 0) {
cy.wait(1000).then(()=>{
recursivelyScroll(done);
})
} else {
// We are done waiting, no more loading is needed
// write test here
done();
}
});
}
TL;DR:
loadingSpinner div toggled on before expensive code, toggled off after
both showLoading() and hideLoading() call log() which writes a message to console.log() and an element's innerHTML
the loadingSpinner and log message in the DOM do not show up before the expensive code is done but the console.log() messages show up when they should
I have a reference to a div stored in loadingSpinner which is just a box that sits above all the other content that should indicate that the site is doing some work. I use these functions to toggle visibility of said div (.hidden is just display: none; in my CSS)
function hideLoading() {
log('hiding')
loadingSpinner.style.display = 'none'
//setTimeout(function (){loadingSpinner.style.display = 'none'}, 10)
//window.getComputedStyle(loadingSpinner) // <-- TRIED FORCING REDRAW
//if (!loadingSpinner.classList.contains('hidden')) {
//loadingSpinner.classList.add('hidden')
//}
}
function showLoading(text) {
log('Showing')
loadingSpinner.innerHTML = text
loadingSpinner.style.display = 'block'
//setTimeout(function (){loadingSpinner.style.display = 'block'}, 10)
//window.getComputedStyle(loadingSpinner)
//if (loadingSpinner.classList.contains('hidden')) {
//loadingSpinner.classList.remove('hidden')
//}
}
function log(s) {
console.log(s)
logDisplay.innerText = s
}
The commented out code are different things I've tried already. The show and hide functions themselves work fine. I can tell that the hide and show functions are called at the right time because of the calls to log().
I have a few instances where the site does some expensive/long running tasks on the client of which nothing should be asynchronous, as far as I can tell (not sure about Array.prototype.forEach()). The Problem is that the loadingSpinner only shows up after the expensive task has completed and then hideLoading() hides it immediately. I did confirm this by adding a setTimeout() to hideLoading().
function updateDOM() {
showLoading('Updating DOM...') // <--- SHOW
log('Updating DOM') // <--- OTHER LOG MESSAGE
codeContainer.innerHTML = '' // <--- start of long running task
codes.forEach(code => {
if (code.base64 === '') {
backgroundQr.set({value: code.data})
code.base64 = backgroundQr.toDataURL()
}
addCodeElement(codeContainer, code)
});
if (codes.length === 0) {
editingId = -1
} // <--- end of long running task
hideLoading() // <--- HIDE
}
Console Log order is correct:
Showing
Updating DOM
hiding
But neither the text that log() writes to the logDisplay-Element nor the loadingSpinner itself show up when they should so I assume it is a rendering issue?
The issue is consistent in multiple places, not just the updateDOM() function.
As expensive code is being executed synchronously, the browser is too busy running it to find any time to render things to the DOM. One approach you can take is to execute expensive code asynchronously using promises and the setTimeout function to delay the expensive execution or send it to the end of the execution queue.
I've created the code snippet below that shows the approach, you'll need:
Spinner handling functions
Expensive executor function
Asynchronous code runner
Your main script that puts them all together
The snippet below contains two examples that you can toggle between, one performs a success execution, by running main();, the other a failure execution, by running main(true);.
function showSpinner() {
document.querySelector('#spinner').style.display = 'block';
}
function hideSpinner() {
document.querySelector('#spinner').style.display = 'none';
}
function executeSuccess() { // runs something expensive that succeeds
return 'data';
}
function executeFailure() { // runs something expensive that fails
throw 'issue';
}
function promiseToRunAsync(executor, ...params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try { resolve(executor(...params)); }
catch (error) { reject(error); }
}, 1000); // arbitrary time that you can set to anything including 0
});
}
function main(failure = false) {
showSpinner(); // show spinner
promiseToRunAsync(failure ? executeFailure : executeSuccess) // execute anync
.then((results) => {
console.log('yay', results);
document.querySelector('#content').innerHTML = results;
hideSpinner(); // hide spinner in case of success
})
.catch((error) => {
console.log('oops', error);
hideSpinner(); // hide spinner in case of failure
});
// ATTN code here will run before the spinner is hidden
}
main(); // executes the success scenario
// main(true); // executes the failure scenario
#spinner {
display: none;
}
<div id="spinner">Spinning...</div>
<div id="content"></div>
NOTE: In the example here, I am adding a 1 second delay to the execution, just to illustrate what's happening, but you'll probably need to set your own wait time or no wait time at all.
I think your problem is that the code is asynchronous.
In the second you start your forEach loop, the code continues all the way to hideLoading while the forEach loop is still executing, therefore you will never see the loader because you call showLoading and hideLoading right after each other.
Try changing your code like this:
function updateDOM() {
showLoading('Updating DOM...') // <--- SHOW
log('Updating DOM') // <--- OTHER LOG MESSAGE
codeContainer.innerHTML = '' // <--- start of long running task
for (const code of codes) {
if (code.base64 === '') {
backgroundQr.set({value: code.data})
code.base64 = backgroundQr.toDataURL()
}
addCodeElement(codeContainer, code)
}
if (codes.length === 0) {
editingId = -1
} // <--- end of long running task
hideLoading() // <--- HIDE
}
Please bear with me I've been dropped into a new project and trying to take it all in. I've been making progress over the last couple of day but can't seem to get over this last hump. Hopefully I can explain it correctly.
I'm loading up a web form and need to make a call out to the API to get some information that may or may not be present based on the data currently loading up. I've simplified my page to basically be this.
...Get some information the user wants and start to do some work to load
up the page and set up the form.
...old stuff working fine...
//Time for my new stuff
var testValue
async function test() {
await http.post(appConfig.serviceRootUrl + '/api/XXX/YYY',
{ mProperty: myObject.collectionInObject.itemInCollection }).then(function (result) {
if (result.length < 1) {
testValue= false;
}
else if (result[0].infoIWant.trim().length > 0) {
testValue= true;
}
});
}
test();
//Originally above in the if I was just seeing if I got a result
//and setting testValue to true/false but changed it for debugging
//and I am surely getting back correct values when the data exists
//or result.length zero when no data for it
...Do a bunch of more stuff that is old and working correctly....
//Test the new stuff up above
alert(testValue);
Most of the time I get back the correct true or false in the alert but once in a while I get back undefined. I'm guessing the undefined is because it is getting to the alert before the async/await finishes. I was under the impression it won't go past the line where I call "test();". I thought it was in effect making it halt anything below test(); until the await finished. Originally it was a bit more complex but I keep stripping it down to make it (hopefully) more basic/simple.
What am I missing in my thoughts or implementation?
Any help greatly appreciated as I'm chasing my tail at this point.
This isn't how async functions work. The function only appears to wait inside the function itself. Outside the function it is called and returns a promise synchronously.
In other words if you write:
let t = test()
t will be a promise that resolves when test() returns. In your current code, if you want to respond outside the function you would need something like:
async function test() {
let result = await http.post(appConfig.serviceRootUrl + '/api/XXX/YYY',
{ mProperty: myObject.collectionInObject.itemInCollection })
if (result.length < 1) return false
else if (result[0].infoIWant.trim().length > 0) return true;
}
// test is an async function. It returns a promise.
test().then(result => alert("result: " + result ))
Edit based on comments
Here's a working version using Axios for the http.post command:
async function test() {
let result = await axios.post('https://jsonplaceholder.typicode.com/posts',
{ mProperty: "some val" })
return result.data
}
// test is an async function. It returns a promise.
test().then(result => console.log(result ))
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
How about doing this?
...Get some information the user wants and start to do some work to load
up the page and set up the form.
...old stuff working fine...
//Time for my new stuff
var testValue
async function test() {
let promise = new Promise( (resolve, reject) => resolve( http.post(appConfig.serviceRootUrl + '/api/XXX/YYY',
{ mProperty: myObject.collectionInObject.itemInCollection }).then(function (result) {
if (result.length < 1) {
testValue= false;
}
else if (result[0].infoIWant.trim().length > 0) {
testValue= true;
}
})));
await promise;
alert(testValue);
}
test();
//Originally above in the if I was just seeing if I got a result
//and setting testValue to true/false but changed it for debugging
//and I am surely getting back correct values when the data exists
//or result.length zero when no data for it
...Do a bunch of more stuff that is old and working correctly....
//Test the new stuff up above
If you use .then() syntax, don't await, and vice versa. I was incorrect about browser compatibility with async/await, seems I haven't kept up with browser scripting, in favor of Node. But also, since you're using jQuery, $.ajax() might be a good option for you, because you don't need async/await or .then(), and you can do like so:
$.ajax(appConfig.serviceRootUrl + '/api/XXX/YYY', {
method: 'POST',
data: { mProperty: myObject.collectionInObject.itemInCollection }
}).done(function(data) {
//use result here just as you would normally within the `.then()`
})
I hope this is more helpful than my original answer.
I have following code snippet.
this.clickButtonText = function (buttonText, attempts, defer) {
var me = this;
if (attempts == null) {
attempts = 3;
}
if (defer == null) {
defer = protractor.promise.defer();
}
browser.driver.findElements(by.tagName('button')).then(function (buttons) {
buttons.forEach(function (button) {
button.getText().then(
function (text) {
console.log('button_loop:' + text);
if (text == buttonText) {
defer.fulfill(button.click());
console.log('RESOLVED!');
return defer.promise;
}
},
function (err) {
console.log("ERROR::" + err);
if (attempts > 0) {
return me.clickButtonText(buttonText, attempts - 1, defer);
} else {
throw err;
}
}
);
});
});
return defer.promise;
};
From time to time my code reaches 'ERROR::StaleElementReferenceError: stale element reference: element is not attached to the page document' line so I need to try again and invoke my function with "attempt - 1" parameter. That is expected behaviour.
But once it reaches "RESOLVED!" line it keeps iterating so I see smth like this:
button_loop:wrong_label_1
button_loop:CORRECT_LABEL
RESOLVED!
button_loop:wrong_label_2
button_loop:wrong_label_3
button_loop:wrong_label_4
The question is: how to break the loop/promise and return from function after console.log('RESOLVED!'); line?
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool, use a plain loop instead.
SOURCE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
Out of curiosity what are you trying to accomplish? To me it seems like you want to click a button based on it's text so you are iterating through all buttons on the page limited by an attempt number until you find a match for the text.
It also looks like you are using protractor on a non-angular page it would be easier if you used browser.ignoreSynchronization = true; in your spec files or the onPrepare block in the conf.js file so you could still leverage the protractor API which has two element locators that easily achieve this.
this.clickButtonText = function(buttonText) {
return element.all(by.cssContainingText('button',buttonText)).get(0).click();
};
OR
this.clickButtonText = function(buttonText) {
return element.all(by.buttonText(buttonText)).get(0).click();
};
If there is another reason for wanting to loop through the buttons I could write up a more complex explanation that uses bluebird to loop through the elements. It is a very useful library for resolving promises.
You are making it harder on yourself by creating an extra deferred object. You can use the promises themselves to retry the action if the click fails.
var clickOrRetry = function(element, attempts) {
attempts = attempts === undefined ? 3 : attempts;
return element.click().then(function() {}, function(err) {
if (attempts > 0) {
return clickOrRetry(element, attempts - 1);
} else {
throw new Error('I failed to click it -- ' + err);
}
});
};
return browser.driver.findElements(by.tagName('button')).then(function(buttons) {
return buttons.forEach(function(button) {
return clickOrRetry(button);
});
});
One approach would be to build (at each "try") a promise chain that continues on failure but skips to the end on success. Such a chain would be of the general form ...
return initialPromise.catch(...).catch(...).catch(...)...;
... and is simple to construct programmatically using the javascript Array method .reduce().
In practice, the code will be made bulky by :
the need to call the async button.getText() then perform the associated test for matching text,
the need to orchestrate 3 tries,
but still not too unwieldy.
As far as I can tell, you want something like this :
this.clickButtonText = function (buttonText, attempts) {
var me = this;
if(attempts === undefined) {
attempts = 3;
}
return browser.driver.findElements(by.tagName('button')).then(function(buttons) {
return buttons.reduce(function(promise, button) {
return promise.catch(function(error) {
return button.getText().then(function(text) {
if(text === buttonText) {
return button.click(); // if/when this happens, the rest of the catch chain (including its terminal catch) will be bypassed, and whatever is returned by `button.click()` will be delivered.
} else {
throw error; //rethrow the "no match" error
}
});
});
}, Promise.reject(new Error('no match'))).catch(function(err) {
if (attempts > 0) {
return me.clickButtonText(buttonText, attempts - 1); // retry
} else {
throw err; //rethrow whatever error brought you to this catch; probably a "no match" but potentially an error thrown by `button.getText()`.
}
});
});
};
Notes :
With this approach, there's no need to pass in a deferred object. In fact, whatever approach you adopt, that's bad practice anyway. Deferreds are seldom necessary, and even more seldom need to be passed around.
I moved the terminal catch(... retry ...) block to be a final catch, after the catch chain built by reduce. That makes more sense than an button.getText().then(onSucccess, onError) structure, which would cause a retry at the first failure to match buttonText; that seems wrong to me.
You could move the terminal catch even further down such that an error thrown by browser.driver.findElements() would be caught (for retry), though that is probably overkill. If browser.driver.findElements() fails once, it will probably fail again.
the "retry" strategy could be alternatively achieved by 3x concatenatation of the catch chain built by the .reduce() process. But you would see a larger memory spike.
I omitted the various console.log()s for clarity but they should be quite simple to reinject.
I am creating an end-to-end test for an Angular 1.5 application using Protractor. The application allows the user to upload a file to a backend Web API, by selecting a file using a standard input type="file" control and a submit button. I have a Controller that basically behaves like this:
function exampleController() {
var vm = this;
vm.status = "";
vm.buttonClickHandler = buttonClickHandler;
function buttonClickHandler() {
vm.status = "calling";
service.makeAsyncCall()
.then(function() {
vm.status = "success";
}, function() {
vm.status = "error";
});
}
}
The buttonClickHandler is called when the user clicks the submit button.
How do I write an end-to-end test using Protractor that verifies that the status changes to "calling" when the user clicks the button, and then to "success" when the promise resolves?
In most of my attempts, I could verify that vm.status had been set to "success", or I could verify that it was set to "calling" if I set ignoreSynchronization = true, but the latter only works when I build in an artificial delay in my Web API backend call, otherwise the process was apparently too fast and the value would show "success".
This is rather ugly, flaky and it's a bad practice but I would give a try anyway :)
it('click test', function () {
$('button').click();
browser.wait(function () {
// Polls 'as fast as possible' until it evalutes to truthy value
// This might be flaky if 'calling' status is shorter than
// 1 polling interval (whose length can't be determined)
return $('div_containing_status').getText().then(function (status) {
return /calling/.test(status);
});
}, 10000);
expect($('div_containing_status').getText()).toBe('calling');
browser.wait(function () {
return $('div_containing_status').getText().then(function (status) {
return /success/.test(status);
});
}, 10000);
expect($('div_containing_status').getText()).toBe('success');
});
UPDATE: edited info about polling based on #alecxe comment