I'm trying to test a website using Puppeteer. Unfortunately, I'm having trouble clicking elements in a toolbar. The toolbar is using a CSS transition to gracefully slide into the page. My code is failing because I'm clicking where the element will appear while the page is still animating. I'm using a timeout as a workaround, but there's got to be a more elegant solution. Here's an example:
await page.click("#showMeetings"); //Toolbar slides in
await page.waitFor(3000); //Can I do this without a timeout?
await page.click("#mtgfind"); //Click button in toolbar
I think I need to wait on the transitionend event, but I'm unsure of how to do that in Puppeteer. Any help would be appreciated.
In case of Grant solution, you shouldn't forget to remove event listener and to wait for it. You can try this solution, it works for me. I had similar problem.
async waitForTransitionEnd(element) {
await page.evaluate((element) => {
return new Promise((resolve) => {
const transition = document.querySelector(element);
const onEnd = function () {
transition.removeEventListener('transitionend', onEnd);
resolve();
};
transition.addEventListener('transitionend', onEnd);
});
}, element);
}
And call it:
await page.click('#showMeetings');
await waitForTransitionEnd('#elementWithTransition');
await page.click("#mtgfind");
I came up with a fairly dumb solution. I looked up how long the transition was supposed to take (in this case 0.2 seconds) and just modified the waitFor statement to wait that long. Final code:
await page.click("#showMeetings"); //Toolbar slides in
await page.waitFor(200);
await page.click("#mtgfind"); //Click button in toolbar
This wouldn't work if the timing was variable, but the website reuses the same transition everywhere, so it's fine for my use case.
You can use page.evaluate() to and the transitionend event to accomplish your goal:
await page.evaluate(() => {
const show_meetings = document.getElementById('showMeetings');
const mtg_find = document.getElementById('mtgfind');
mtg_find.addEventListener('transitionend', event => {
mtg_find.click();
}, false);
show_meetings.click();
});
Related
edit: ATTENTION: following the recommendation by user Barmar in their comment below i will vote to close this more or less opinion/best practise oriented question. I did not delete it but voted to close for there have been answers and productive comments with upvotes already. I reposted this question on https://codereview.stackexchange.com/questions/250351/how-to-await-user-input-with-javascripts-async-await-syntax-in-a-more-functional
I love async/await and Promises since I got my hands on them. And I might be overdoing it, but it feels like there should be a good and readable way to utilize async/await to get a little closer to a flow like, functionalISH programming style.
I would love to not have to only use async/await to wait for web resources to come back but also to wait for user input when I await it.
So far i have some code working similar to this shortened demo where I wrap a one time only EventListener into a Promise:
//// MAIN ///////
(async function(){
//...do some async await stuff here... fetching stuff from a server
// let services = await fetch(...) for example
let services = [{url:"x",label:"1"},{url:"y",label:"2"},{url:"z",label:"3"}]
let service_chosen = await showServicesToUserAndAwaitInput(services);
console.log("service_chosen:",service_chosen);
// ... go on....
})()
//// END MAIN /////
async function showServicesToUserAndAwaitInput(services){
if (services.length < 1){return null}
let choice = null;
let serviceList = document.querySelector("#serviceList");
// show list element
serviceList.classList.remove("hidden")
// create some elements for the user to interact with
for (let service of services){
let button = document.createElement("BUTTON");
button.innerHTML = service.label;
button.addEventListener("click",function(){
document.dispatchEvent(
new CustomEvent('serviceChosen', { detail:service })
)
});
serviceList.appendChild(button);
}
// returns promise with one time only event listener
return new Promise((resolve,reject)=>{
document.addEventListener("serviceChosen",function(e){
serviceList.classList.add("hidden") // hide again for we are done
resolve(e.detail)
},{ once: true })
})
}
.hidden{
visibility: hidden
}
<div id="serviceList" class="hidden">
</div>
But something about this use of the EventListener bugs me. Also: I use a promise that always resolves, which also seems strange.
On the upside: I get to read the code top to bottom and can follow the user flow within the MAIN without having to chase down events, callbacks and so on.
Yet, it feels like I am reinventing something somebody else might have already normed. So:
Is there a better way to achieve this? Are there best practices to work with user interactions or other DOM events in a async and/or thenable way?
So here is an example of your code without async/await and custom events. Progress is done simply by calling appropriate functions. User input is handled by the click event, which listenes for user interaction and triggers appropriate functions.
//// MAIN ///////
let services = [{url:"x",label:"1"},{url:"y",label:"2"},{url:"z",label:"3"}]
showServicesToUserAndAwaitInput(services);
//// END OF MAIN ////
function showServicesToUserAndAwaitInput(services){
if (services.length < 1){return null}
let choice = null;
let serviceList = document.querySelector("#serviceList");
// show list element
serviceList.classList.remove("hidden")
// create some elements for the user to interact with
for (let service of services){
let button = document.createElement("BUTTON");
button.innerHTML = service.label;
button.addEventListener("click",function(){
chooseService(service);
});
serviceList.appendChild(button);
}
function chooseService(service) {
let service_chosen = service;
console.log("service_chosen:",service_chosen);
hideServices();
}
function hideServices() {
serviceList.classList.add("hidden") // hide again for we are done
}
}
.hidden{
visibility: hidden
}
<div id="serviceList" class="hidden">
</div>
I came across the following code:
let timeoutHandler;
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(() => {...});
This is an overly simplification since the original code is contained in a Vue application as follow:
public handleMultiSelectInput(value): void {
if (value === "") {
return;
}
clearTimeout(this.inputTimeoutHandler);
this.inputTimeoutHandler = setTimeout(() => {
axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`)
.then(response => {
console.log(response);
})
}, 400);
}
Does this mean this is some kind of cheap-ass debounce function?
Could someone explain what this exactly means.
Yes, it is a debounce function, which is when we wait for some amount of time to pass after the last event before we actually run some function.
There are actually many different scenarios where we might want to debounce some event inside of a web application.
The one you posted above seems to handle a text input. So it's debouncing the input, meaning that instead of fetching that endpoint as soon as the user starts to enter some character into the input, it's going to wait until the user stops entering anything in that input. It appears it's going to wait 400 milliseconds and then execute the network request.
The code you posted is kind of hard to read and understand, but yes, that is the idea of it.
I would have extracted out the network request like so:
const fetchData = async searchTerm => {
const response = await axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`);
console.log(response.data);
}
let timeoutHandler;
const onInput = event => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
}
timeoutHandler = setTimeout(() => {
fetchData(event.target.value);
}, 400);
}
Granted I am just using vanilla JS and the above is inside a Vuejs application and I am not familiar with the API the user is reaching out to. Also, even what I offer above could be made a lot clearer by hiding some of its logic.
Is there any way or Puppeteer API we can wait element to disappear or remove from Dom and then continue the execution?
E.g I have a loading animation I want to wait until this loading animation remove from Dom.
waitForSelector has a hidden option which also check if the element is in the DOM:
await page.waitForSelector('div', {hidden: true});
Try this
await page.waitForFunction(() => !document.querySelector(querySelector));
If you're waiting for API response, maybe it's better to rely on
await page.waitForResponse(response => response.url() === myUrl && response.status() === 200);
You can use page.waitForFunction with a conditional statement.
await page.waitForFunction('document.querySelector("#myElement") === null')
https://pptr.dev/#?product=Puppeteer&version=v5.2.1&show=api-pagewaitforfunctionpagefunction-options-args
I have been working on a Puppeteer system to help me handle my automation testing. However, my page would prompt a random pop-up to notify customers of on-going promotions - This prevents my test from proceeding. My initial thought was to run a endless loop in the background during the test to waitForSelector and click if it exists. However, I was thinking that this approach doesn't sound too smart, and I couldn't find anything suitable in the API.
Has anyone encountered the similar problem and has come out with a brilliant solution?
If the popup is always going to appear towards the beginning of the session, you can use page.waitForSelector():
await page.waitForSelector('#popup', {visible: true});
await page.click('#popup'); // Close Popup
Alternatively, if the element is dynamically added to the page and might not appear, you can use the MutationObserver interface to watch for the element to be added to the DOM tree and click it:
await page.evaluate(() => {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
for (let i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].id === 'popup' && window.getComputedStyle(mutation.addedNodes[i]).display !== 'none') {
mutation.addedNodes[i].click(); // Close Popup
}
}
});
});
observer.observe(document, {subtree: true});
});
I'm trying to make a javascript script running from the urlbar which for each row on a specific page clicks a button (allowing me to change a certain value), then edit that value, then click that button again to confirm. I'm running into a lot of trouble with simultaneity, as it seems to wait to do the first click 5000 ms even tho it's not being told to pause. didn't do the same when i tested the structure replacing things in between pauses with rando alerts
The reason for the pauses is bc there's a short loading animation during which I can't execute code. Anything glaringly wrong in this as-is? I know there's funky stuff w/ closure, but I thought by busywaiting i could get past it but its not.
function pause(milliseconds) {
var dt = new Date();
while ((new Date()) - dt <= milliseconds) { /* Do nothing */ }
}
var q=document.getElementById("MainContent_gvProducts").children[0];
var l=q.children.length;
for(i=1;i<l;i++){
q.children[i].children[0].children[0].click();
pause(5000);
var x=q.children[i].children[7].children[0].value;
pause(1);
x=math.round(x);
pause(5000);
q.children[i].children[0].children[0].click();
pause(5000);
}
Your pause function seems very hacky.
You should use setTimeout() instead, or something similar.
If you can afford to use the latest JS tech, I'd recommand creating a promise with a timeout and using async/await, with something like this :
async function pause(t = 1000) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, t)
})
}
for(i=1;i<l;i++){
q.children[i].children[0].children[0].click();
await pause(1000)
var x=q.children[i].children[7].children[0].value;
pause(1);
x=math.round(x);
await pause(5000);
q.children[i].children[0].children[0].click();
await pause(5000);
}
Note : your for loop must be placed in an async function.