
Give Your Optimizely Devs (and Agents) Visual Guardrails to Prevent Bugs. Save your spot for the Oct. 29th webinar
Have you ever encountered a situation where you try to take a screenshot, but instead of the beautifully well-crafted UI, all you’ve got is an image of a spinner/skeleton/loading screen? Handling animations and loading artifacts in visual testing can be daunting.
Don’t worry – it can happen to anyone, and we’re not here to judge you 😉
One of the SDK engineers here at Applitools, Noam, breaks this down into a hands-on tutorial, hoping it will help you get a better understanding of the industry’s best practices around visual testing of rich and dynamic UI experiences.
Let’s dive right in!
Most frameworks already have different mechanisms to handle animations and loading artifacts. Keeping things simple is often the best way to achieve code stability and maintainability. Using your framework’s built-in tools would most often be the best approach.
For example:
// Playwright: wait for spinner to be removed
await page.waitForSelector('.spinner', { state: 'detached' });
await eyes.check()
// Cypress: wait for spinner to not exist
cy.get('.spinner').should('not.exist');
cy.eyesCheck()
// Selenium: wait for spinner to be invisible
const spinnerElements = await driver.findElements(By.css('.spinner'));
if (spinnerElements.length) {
await driver.wait(until.elementIsNotVisible(spinnerElements[0]), 5000);
}
await eyes.check()
Even if the UI appears visually unchanged, frontend frameworks like React, Vue, and Angular may re-render elements under the hood. This can lead to stale element references, especially when capturing regions right after a DOM change.
Consider the following example:
cy.get('.main').then($el => {
cy.get('.spinner').should('not.exist'); // spinner disappears after main was located
cy.eyesCheckWindow({
tag: 'main',
target: 'region',
element: $el, // stale reference if .main was replaced
});
});
.mainWhen possible (e.g., Playwright), it’s preferred that you use locators instead of selectors. If you can’t, it’s better if you use selectors instead of DOM references (element: ‘.main’).
There are many techniques to eliminate other types of dynamic behaviour in web pages. Playwright, for example, provides a Clock API that allows pausing JavaScript time-related events (including JS-driven animations). It’s also possible to install custom CSS snippets to pause and reset CSS-related animations. Other JS specialized crafted snippets would be required for resetting GIFs, videos, and so on – you get the idea.
This never-ending cat-and-mouse game can be prevented by using Applitools Ultrafast Grid (UFG). Instead of rendering web pages on locally executed browsers, the UFG team maintains specialized logics and fine-tuned commands that ensure a stable and consistent rendering experience. While UFG offers more than just rendering stability, it’s worth noting that classic screenshots can still achieve stable results. UFG just makes it easier!
If you intentionally want to capture dynamic content (e.g., animations, changes), a smarter strategy is to embrace that variability and use smart matching algorithms to compare just what you need, like those found in Applitools Eyes.
Any match level can be used for the entire screenshot or specific regions of the screen. Read more in the Match Level Best Practices tutorial. For example, algorithms like the Layout match level can drastically improve your experience with localization testing.
waitBeforeCapture SettingPerforming wait operations can become more complicated when:
The waitBeforeCapture setting was invented for these types of use cases (and a few others).
This setting can receive three types of arguments:
// eyes-storybook
waitBeforeCapture: async () => {
while (document.querySelector('.spinner')) {
await new Promise((resolve) => setTimeout(resolve, 100))
}
return true;
}
// eyes-playwright
await eyes.check({
name: 'my-step',
async waitBeforeCapture() {
await page.locator('.spinner').waitFor({state: 'hidden'})
},
})
The waitBeforeCapture setting can be defined in your applitools.config file, in your eyes.check settings, using the Target settings builder, and in other similar places. Please refer to the documentation of the specific SDK you’re using for concrete examples.
A nice eyes-storybook-specific trick to achieve a desired rendering state would be a Storybook Play Function.
Applitools Eyes will run your play functions and wait for them to finish before capturing anything on the screen. Use Play Functions to navigate the story to an interesting state and wait for the story to be stable inside the play function to help Eyes understand what the best time is to capture the screenshot.
We hope you’ve found this article interesting, and maybe it solved some of the most common visual testing issues you may have encountered. Go ahead and try these examples out for free with Applitools Eyes.
However, if something isn’t clear or if you’d like advice regarding the best way to incorporate visual testing into your organization, please don’t hesitate to reach out to our experts! Testing is our passion, and we’re here to help.
Loading artifacts are transient UI elements, like spinners, skeleton cards, GIFs, that appear while data is fetched. If a screenshot is captured before they disappear, your baseline image won’t match future renders, causing false failures (flaky tests).
Modern frameworks often replace DOM nodes even when the UI looks identical. If you save a DOM reference (e.g., cy.get('.main')) before waiting for the spinner to vanish, that reference may point to a removed element, causing stale errors. Capture by selector or locator, not by saved element handles, to avoid this.
waitBeforeCapture delays the screenshot after the DOM is stable. It accepts:
• Milliseconds (e.g., 500)
• CSS selector to wait for element presence/absence
• Custom async function for complex logic (e.g., loop until .spinner hidden)
Yes. In eyes‑storybook, Applitools runs each story’s Play Function and waits for it to finish—perfect for clicking buttons, filling forms, or pausing animations before the snapshot.
Fast-forwarding the JS clock (e.g., page.clock.fastForward(1000) in Playwright) is usually more reliable and efficient than using hard timeouts. It advances timers without waiting in real time, making tests faster. However, it won’t affect CSS-driven animations since those still require CSS overrides to pause or skip transitions. For full stability, combine clock control with style injections or use Applitools Ultrafast Grid, which auto-handles CSS animations under the hood.