Hello, is it possible to perform a waitForSelector until something is hidden even if it rapidly appears and disappears multiple times? This would be similar to a debounce feature or very much like the waitForLoadState with networkidle selected. I would want to set the debounce to something like 500 milliseconds but have it configurable.
I have looked in the issues and couldn't find anything like this but this would help with loading divs and animations that popup multiple times to ensure that we don't execute the next statement until the threshold has been met.
Thoughts?
@mattduffield good point.
You probably can do something like this:
public async waitForSelectorToCompletelyDisappear(timeout = 500) {
const startTime = new Date();
let duration = 0;
const promise = page
.waitForSelector(`[data-test-id="test"]`, { timeout, state: 'hidden' })
.then(async () => {
return new Date();
});
duration = (await promise).getTime() - startTime.getTime();
while (duration < timeout) {
duration = (await promise).getTime() - startTime.getTime();
}
}
Hi @DJ-Glock , thanks for your code example. I tried your example but it seems to be caught in a very long loop. When I debugged it after it was hanging for over a minute, the duration was on the value 5?
@mattduffield I guess the closest what playwright has to offer today is elementHandle.waitForElementState which allows you to wait until the element is "stable". Sounds like you need a variation of the method which would wait until the element preserves its visibility state for some period, am I getting it right?
@yury-s Yes, exactly. I have several instances where the developers are rapidly displaying loading indicators. They happen so fast that it seems a debounce with 500 milliseconds would do the trick.
We are not sure yet if this is worth adding to the API given that many scenarios could be implemented on the client side and the main value that playwright could provide in that case is the ability to use more advanced selectors. We'll probably wait for more feedback to decide.
In the meantime could you try waiting "manually" by running something like this:
await page.$eval('selector', element => {
const isVisible = () => {
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
let fulfill;
const result = new Promise(f => fulfill = f);
let lastState = isVisible();
let startTime = Date.now();
const pollState = () => {
const visible = isVisible();
if (lastState === visible) {
if (Date.now() - startTime > 500) {
fulfill();
return;
}
} else {
lastState = visible;
startTime = Date.now();
}
requestAnimationFrame(pollState);
};
requestAnimationFrame(pollState);
return result;
});
Hi @yury-s thanks for the sample! I tried it out and it worked exactly as I expected. I made a slight modification so that I could pass in the timeout:
const waitForHiddenDebounce = async (selector, timeout = 500) => {
await page.$eval(selector, (element, timeout) => {
const isVisible = () => {
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
let fulfill;
const result = new Promise(f => fulfill = f);
let lastState = isVisible();
let startTime = Date.now();
const pollState = () => {
const visible = isVisible();
if (lastState === visible) {
if (Date.now() - startTime > timeout) {
fulfill();
return;
}
} else {
lastState = visible;
startTime = Date.now();
}
requestAnimationFrame(pollState);
};
requestAnimationFrame(pollState);
return result;
}, timeout);
};
Thanks again for your help! Having debounce with multiple busy indicators displaying rapidly makes it so much nicer to test.
@mattduffield
Apologies for posting bad example. I can see that Yury already provided you working code, but let me also finish my try :)
Tested it with static elements (not appearing many times), but should work for you.
/** Returns true if element disappeared after provided timeout. */
async function waitForSelectorToCompletelyDisappear(selector, timeout = 500, pollingTimeout = 100) {
const startTime = Date.now();
let duration = 0;
let isDisappeared = false;
async function endTime() {
let now = await page.waitForSelector(selector, { timeout, state: 'hidden' })
.then(async () => {
console.log(`disappeared`);
isDisappeared = true;
return Date.now();
})
.catch(async () => {
console.log(`visible`);
isDisappeared = false;
return Date.now();
})
return now;
}
duration = (await endTime()) - startTime;
while (duration < timeout) {
duration = (await endTime()) - startTime;
await page.waitForTimeout(pollingTimeout);
}
console.log(`Disappeared finally: ${isDisappeared}`);
return isDisappeared;
}
Hi @DJ-Glock thanks for the update. I will go ahead and test your version as well!
@DJ-Glock I have tested your version and it works as well! Thanks for the update.
Most helpful comment
Hi @yury-s thanks for the sample! I tried it out and it worked exactly as I expected. I made a slight modification so that I could pass in the timeout:
Thanks again for your help! Having debounce with multiple busy indicators displaying rapidly makes it so much nicer to test.