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!
Framework Native Solutions
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 JS
// Playwright: wait for spinner to be removed
await page.waitForSelector('.spinner', { state: 'detached' });
await eyes.check()
Cypress
// Cypress: wait for spinner to not exist
cy.get('.spinner').should('not.exist');
cy.eyesCheck()
Selenium JS
// 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()
A Common Pitfall
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
});
});
- First, Cypress locates
.main
- Then, Cypress waits for the spinner to disappear
- This example would fail (even if the new element has the exact same properties) if the main element is replaced by another element
How to avoid that?
When 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’
).
Videos, CSS Animations, GIFs
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!
Algo-Based Solutions
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.
The waitBeforeCapture
Setting
Performing wait operations can become more complicated when:
- Testing with no-code visual testing SDKs (e.g eyes-storybook)
- Testing with advanced Eyes features like lazyLoading and layoutBreakpoints
The waitBeforeCapture
setting was invented for these types of use cases (and a few others).
This setting can receive three types of arguments:
- Milliseconds – the simplest approach. While it’s not always the most innovative or sophisticated pattern, in many cases, it “does the trick.” In general, waiting for explicit timeouts during tests is not recommended. However, when compared to clock manipulations and code injections, sometimes the simplicity and stability is worth the longer run-time.
- Selector – when we’re waiting for something to appear, most SDKs support passing a selector, and Applitools Eyes will automatically wait for an element that matches this selector to appear in the web page.
- Custom function – see code example
// 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.
Storybook Play Functions
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.
Applitools is Here to Help
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.
Quick Answers
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.
Is it better to fast-forward the JavaScript clock or add explicit waits for CSS animations?
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.