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();
}
});
}
Related
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
}
These examples are going to be super simplified but hopefully you get the gist.
Say I have a function as so, which runs a series of tasks and can take a long time:
async function doSomeStuff() {
await aLongTask();
await anotherBigOldTask();
await bigNestedTaskThatTakesForever();
return Promise.resolve('Done');
}
Now I have a chokidar watch which looks for file changes and runs said function:
const watcher = chokidar.watch(watchURLs, {
ignored: /\.git/,
ignoreInitial: true,
persistent: true,
ignorePermissionErrors: true,
atomic: 500,
});
watcher.on('all', async (event, filePath) => {
await doSomeStuff();
});
The issue is that when the files are changed many times whilst doSomeStuff() is running, it tries to run the build multiple times. This causes a whole wealth of errors, which I've crudely fixed with a timeout lock:
if (!locked) {
await doSomeStuff().finally(() => {
locked = false;
info('Waiting for file changes in', ...watchURLs);
});
}
else {
debug('Attempting to trigger a locked build');
}
locked = true;
clearTimeout(lockTimeout);
lockTimeout = setTimeout(() => {
locked = false;
debug('Unlocked due to timeout');
}, 10000);
This stops stuff from completely imploding, but it means that everything will always be out of date. It's only when saving a file after the lock grace period is over that it will be picked up. And if a load of files are saved, it could build with just half of the updated ones included.
So, how can the doSomeStuff() function be completely cancelled/halted/rolled-back immediately before a new build is triggered? I don't want to do something where I'm setting a variable that causes doSomeStuff() to return that I wrap around every task, because that's not immediate, and doesn't work for any chuggy functions nested inside. It's almost as if I want to throw an error into a function, rather than that function controlling what to throw for itself.
Can anyone think of a way of immediately forcing a function to stop executing without killing the whole script (a la process.exit())? Or if there's just a simple way of doing this kind of thing? TIA.
Throwing in an error actually sounds good, that can be done quite elgantly:
const cancellable = fn => (...args) => {
let cancel = false;
const breakpoint = () => {
if(cancel) throw new Error("Cancelation");
};
return {
result: fn(breakpoint, ...args).catch(console.log),
stop() { cancel = true; },
};
};
That can be used as:
const doSomeStuff = cancellable(async function doSomeStuff(breakpoint) {
await aLongTask();
breakpoint();
await anotherBigOldTask();
breakpoint();
await bigNestedTaskThatTakesForever();
return "Done";
});
let task = doSomeStuff();
task.stop();
No, what you are looking for is not possible. There is no "task" abstraction that implicitly wraps something, and you cannot interrupt arbitrary functions. They need to explicitly opt into being cancellable (e.g. by checking a breakpoint that is passed to them, like in Jonas' answer). You can do that using a child process which you can kill, but that might leave inconsistent state behind.
Let's break it down:
Promise Cancellation This does exist. You need a polyfill to add support for .cancel which when called will cancel the Promise execution. You can read more about this here. Once you install a polyfill, your promise will have a .cancel so you can have:
const doSomeStuff = () => {
return new Promise(resolve => {
...
})
}
let prmise = null;
const watcher = chokidar.watch(watchURLs, {
ignored: /\.git/,
ignoreInitial: true,
persistent: true,
ignorePermissionErrors: true,
atomic: 500,
});
watcher.on('all', (event, filePath) => {
promise = doSomeStuff();
});
watcher.on('change', () => {
promise && promise.cancel()
});
Rollbar This does not exist. You'd have to implement this yourself. Maybe take a snapshot of the state at the very beginning of doSomeStuff and then if .cancel was called, update the state to match the beginning state. But a "rollback" is not a predefined thing that can have native support, unlike databases where a rollback has a pretty good definition.
I have a function that solves a puzzle in a browser. It might take a very long time to finish and I want to stop its execution if the runtime exceeds 30 seconds. Something like this:
function solve(puzzle) {
// A loop to solve a puzzle that might take minutes to run
while (notSolve){
keepFindingSolution(); // This is not a loop
}
return solution;
}
let solution = solve(veryHardPuzzle);
if (solution && solution.solved) {
// Do something
}
console.log("Other executions");
So, I don't want to block the UI thread when solving the function so that users can still interact with the website. I also want to get the solution from the solve(veryHardPuzzle) once it's done, or break the execution of the function if the time is out.
I tried some different approaches below, but none of them work for me:
Wrap the solve() function in setTimeout()
setTimeout(function () {
let solution = solve(veryHardPuzzle);
if (solution && solution.solved) {
// Do something
}
}, 30000);
console.log("Other executions");
This approach (from https://stackoverflow.com/a/26616321/6308776) doesn't block the UI thread, and the other executions are executed happily. However, it just basically waits 30 seconds before executing the inner solve(veryHardPuzzle) function (please correct me if I'm wrong). If the solve(veryHardPuzzle) function runs longer than 30 seconds then it would block the thread.
clearTimeout() once the solve() is done
let timerId = setTimeout(function () {
let solution = solve(veryHardPuzzle);
clearTimeout(timerId);
if (solution && solution.solved) {
// Do something
}
}, 30000);
console.log("Other executions");
I thought that this would stop the timeout() after the solution is found, but it technically waits 30 seconds before the solver(veryHardPuzzle) is executed.
After doing some research, I realized setTimeout() might not the thing I want. Do you have any ideas or techniques on how to solve this?
The easiest way to handle asynchronous (like) behavior is with a Promise.
In this case you could do something like this:
function doLongThing() {
let success = false;
return new Promise(function(resolve, reject) {
//set timeout so if success is false after 30 seconds, reject promise.
setTimeout(function () {
if(!success) reject();
}, 30000)
//another set timeout to prevent blocking UX thread
setTimeout(function () {
//do long running thing
let solution = solve(veryHardPuzzle);
//thing finished
success = true;
//pass result into resolve method so consumer can use it
resolve(solution);
});
})
}
then you could do this:
doLongThing().then(function(solution) {
//do something with solution
}).catch(function() {
//timed out
});
console.log("Other executions");
A few comments have hinted at this, but the answer is not in how you call solve, but rather in how solve iterates over each “try” (I’m assuming this is how your solver works). If after each try your solver will break out of the event loop (the essence of async JS), then the main thread will have opportunities to do the other work it needs to do like handle mouse clicks or paint the UI. There are a few ways to do this, but maybe Promises is the easiest to visualize and has the best browser support (though some polyfills may be necessary for some IE versions).
var timeIsUp = false
function trySolution (puzzle, solution) {
return new Promise(function (resolve, reject) {
if (!solution) reject('no solution found')
if (timeIsUp) reject('time up!')
if (puzzle + solution !== 'wrong') { // idk lol
resolve({solution: solution, success: true})
} else {
resolve({solution: solution, success: false})
}
}
}
// solve is now recursive with each iteration async
function solve (puzzle, solutions) {
trySolution(puzzle, solutions[0])
.then(function (result) {
if (result.success) {
solve(puzzle, solutions.slice(1))
}
})
.catch(function (err) {
console.error(err)
})
}
// do the deed
solve(puzzle, solutions) // puzzle is undefined error lol
setTimeout(function () {
timeIsUp = true
}, 30000)
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.
For a simple web app that needs to refresh parts of data presented to the user in set intervals, are there any downsides to just using setInterval() to get a JSON from an endpoint instead of using a proper polling framework?
For the sake of an example, let's say I'm refreshing the status of a processing job every 5 seconds.
From my comment:
I would use setTimeout [docs] and always call it when the previous response was received. This way you avoid possible congestion or function stacking or whatever you want to call it, in case a request/response takes longer than your interval.
So something like this:
function refresh() {
// make Ajax call here, inside the callback call:
setTimeout(refresh, 5000);
// ...
}
// initial call, or just call refresh directly
setTimeout(refresh, 5000);
A simple non-blocking poll function can be implemented in recent browsers using Promises:
var sleep = duration => new Promise(resolve => setTimeout(resolve, duration))
var poll = (promiseFn, duration) => promiseFn().then(
sleep(duration).then(() => poll(promiseFn, duration)))
// Greet the World every second
poll(() => new Promise(() => console.log('Hello World!')), 1000)
You can do just like this:
var i = 0, loop_length = 50, loop_speed = 100;
function loop(){
i+= 1;
/* Here is your code. Balabala...*/
if (i===loop_length) clearInterval(handler);
}
var handler = setInterval(loop, loop_speed);
Just modify #bschlueter's answer, and yes, you can cancel this poll function by calling cancelCallback()
let cancelCallback = () => {};
var sleep = (period) => {
return new Promise((resolve) => {
cancelCallback = () => {
console.log("Canceling...");
// send cancel message...
return resolve('Canceled');
}
setTimeout(() => {
resolve("tick");
}, period)
})
}
var poll = (promiseFn, period, timeout) => promiseFn().then(() => {
let asleep = async(period) => {
let respond = await sleep(period);
// if you need to do something as soon as sleep finished
console.log("sleep just finished, do something...");
return respond;
}
// just check if cancelCallback is empty function,
// if yes, set a time out to run cancelCallback()
if (cancelCallback.toString() === "() => {}") {
console.log("set timout to run cancelCallback()")
setTimeout(() => {
cancelCallback()
}, timeout);
}
asleep(period).then((respond) => {
// check if sleep canceled, if not, continue to poll
if (respond !== 'Canceled') {
poll(promiseFn, period);
} else {
console.log(respond);
}
})
// do something1...
console.log("do something1...");
})
poll(() => new Promise((resolve) => {
console.log('Hello World!');
resolve(); //you need resolve to jump into .then()
}), 3000, 10000);
// do something2...
console.log("do something2....")
I know this is an old question but I stumbled over it, and in the StackOverflow way of doing things I thought I might improve it. You might want to consider a solution similar to what's described here which is known as long polling. OR another solution is WebSockets (one of the better implementations of websockets with the primary objective of working on all browsers) socket.io.
The first solution is basically summarized as you send a single AJAX request and wait for a response before sending an additional one, then once the response has been delivered, queue up the next query.
Meanwhile, on the backend you don't return a response until the status changes. So, in your scenario, you would utilize a while loop that would continue until the status changed, then return the changed status to the page. I really like this solution. As the answer linked above indicates, this is what facebook does (or at least has done in the past).
socket.io is basically the jQuery of Websockets, so that whichever browser your users are in you can establish a socket connection that can push data to the page (without polling at all). This is closer to a Blackberry's instant notifications, which - if you're going for instant, it's the best solution.