Playwright: How to Wait Until There Is No Animation on the Page?
I'm Trying to Figure out How to Check Whether the Page Has Fully Loaded in Playwright. Await Page. waitForLoadState('networkidle'); Doesn't Always Work for Me...
I'm trying to figure out how to check whether the page has fully loaded in Playwright. await page.waitForLoadState('networkidle'); doesn't always work for me on Javascript-heavy sites. I've resorted to taking a screenshot base64, waiting 100 ms, taking a new screenshot, and comparing whether those are the same. However this doesn't seem ideal, is there any way to ask Playwright when the last animation frame was redrawn?
7 Answers
There are several options that may help you.
1. Solution 1:
First, you can maybe determine which element is loading last, and then go with
page.waitForSelector('yourselector')
or even wait for multiple selectors to appear
page.waitForSelector('yourselector1','yourselector2')
2. Solution 2
page.waitForLoadState('domcontentloaded')
Because
page.waitForLoadState('networkidle')
by default will wait for network event and if 0.5 seconds nothing is network trafficking it will say, I am no longer need to wait. And that is why you maybe have stohastic beh.
doesn't always work for me on Javascript-heavy sites
In my case the page was a SPA, and was changing even after it was reported as loaded.
I used a basic debouncer: it waits for a specific portion of the DOM (that I'm interested in) to stop changing for 1000 milliseconds.
public async Task DebounceDom(ILocator locator, int pollDelay = 100, int stableDelay = 1000)
{
var markupPrevious = "";
var timerStart = DateTime.UtcNow;
var isStable = false;
while (!isStable)
{
var markupCurrent = await locator.InnerHTMLAsync();
if (markupCurrent == markupPrevious)
{
var elapsed = (DateTime.UtcNow - timerStart).TotalMilliseconds;
isStable = stableDelay <= elapsed;
}
else
{
markupPrevious = markupCurrent;
}
if (!isStable) await Task.Delay(pollDelay);
}
}
I'm using the .NET Playwright bindings, but you could easily translate that to JS or Python.
It's potentially expensive, so don't use it unless you must, and choose the delays wisely.
Typescript version based on @lonix answer. Also I've used a general approach polling whole document.body instead of particular DOM node, as in my case it wasn't working - markupCurrent was stable starting with page load while the animations continued to perform.
async debounceDom(pollDelay = 50, stableDelay = 350) {
let markupPrevious = '';
const timerStart = new Date();
let isStable = false;
while (!isStable) {
const markupCurrent = await this.page.evaluate(() => document.body.innerHTML);
if (markupCurrent == markupPrevious) {
const elapsed = new Date().getTime() - timerStart.getTime();
isStable = stableDelay <= elapsed;
} else {
markupPrevious = markupCurrent;
}
if (!isStable) await new Promise(resolve => setTimeout(resolve, pollDelay));
}
}
Self-explaining method for those want to check what is going on with the page
async debounceDomLog(pollDelay = 50, stableDelay = 350) {
let markupPrevious = '';
const timerStart = new Date();
let isStable = false;
let counter = 0;
while (!isStable) {
++counter;
console.log('-----\nattempt: ', counter);
const markupCurrent = await this.page.evaluate(() => document.body.innerHTML);
const elapsed = new Date().getTime() - timerStart.getTime();
if (markupCurrent == markupPrevious) {
isStable = stableDelay <= elapsed;
if (!isStable) {
console.log('size is stable! Still polling... (Reason: delay not elapsed yet)');
console.log('this attempt size: ', markupCurrent.length);
} else {
console.log('size settled and time elapsed');
console.log('this attempt size: ', markupCurrent.length);
}
} else {
console.log('this attempt size: ', markupPrevious.length);
markupPrevious = markupCurrent;
}
if (!isStable) {
console.log('waiting for poll delay: ', pollDelay);
await new Promise(resolve => setTimeout(resolve, pollDelay));
}
console.log('elapsed: ', elapsed)
}
}
and the resulting output:
-----
attempt: 1
this attempt size: 0
waiting for poll delay: 25
elapsed: 113
-----
attempt: 2
this attempt size: 1612125
waiting for poll delay: 25
elapsed: 208
-----
attempt: 3
size is stable! Still polling... (Reason: delay not elapsed yet)
this attempt size: 1608951
waiting for poll delay: 25
elapsed: 304
-----
attempt: 4
size settled and time elapsed
this attempt size: 1608951
elapsed: 400
1 passed (11.5s)
A current solution could be to take a screenshot at the beginning with animations disabled.
await page.getByRole('link')
.screenshot({ animations: 'disabled' });
(talking about animations not loading content)
If the given solution doesn't work for you, you can try with locator.
page.locator(selector[, options])
It has multiple api like locator.isDisabled or locator.waitFor([options]) or locator.isVisible([options]) or locator.frameLocator(selector) ....... a lot more.
see the below link here:
If you really need to wait for the page to stop visually changing, you can write a simple function that will take the page screenshot, compare it with the previous one and quit if they are equal.
I use this function:
def wait_animation_stops(page, timeout=30):
t = time()
old_image = page.screenshot(full_page=True)
while time() - t < timeout:
sleep(0.5)
new_image = page.screenshot(full_page=True)
if old_image == new_image:
print(f'Animation stopped after {time() - t} seconds')
return True
old_image = new_image
print(f'Animation did not stop in {timeout} seconds')
return False
How to wait till all animations has finished loading?
You can wait till the animations on last(visually) loaded element has finished:
import type { Page } from '@playwright/test'
function waitForAnimationEnd(page: Page, selector: string) {
return page
.locator(selector)
.evaluate((element) =>
Promise.all(
element
.getAnimations()
.map((animation) => animation.finished)
)
)
}
test(`Wait till last element animations has finished loading `, async ({ page }) => {
await wait_for_animation_end(page, `lastLoadedElement`)
})
Reference: