Solving Flaky UI Tests with Cypress Automatic Waiting
Stop using manual sleep timers in your UI tests. Learn how Cypress automatic waiting and retry-ability eliminate flakiness by polling the DOM until assertions pass.
07 Aug 2025, 20:44 UTC

The Problem: The 'Race Condition' in UI Testing
In modern single-page applications (SPAs), the DOM is dynamic. Elements appear, disappear, or re-render based on asynchronous API calls or JavaScript timers. A common failure occurs when a test attempts to interact with an element that hasn't finished rendering yet, leading to the dreaded "element not found" error. The traditional fix is to sprinkle cy.wait(2000) throughout the code, but this slows down the suite and creates brittle tests that fail if the network is slightly slower than 2 seconds.
Thesis: Retry-ability as a First-Class Citizen
Cypress solves this by treating DOM queries and assertions as a single, retryable unit. Instead of failing immediately, Cypress automatically re-runs the command chain until the target element meets the required condition or a timeout is reached. This removes the need for manual sleep timers and ensures tests run as fast as the application allows.
How the Retry Mechanism Operates
When you chain a command like cy.get() with an assertion like .should('be.visible'), Cypress doesn't just run them once. It follows a specific loop:
- The Query: Cypress searches the current DOM for the selector.
- The Assertion: If the element is found, Cypress checks if it is visible.
- The Loop: If the element is missing or the assertion fails, Cypress waits a few milliseconds and restarts the entire chain from the
cy.get()command. - The Resolution: This continues until the assertion passes or the default timeout (typically 4 seconds) expires.
This is critical for frameworks like React or Vue, where an element might be detached and re-attached to the DOM during a state update. Because Cypress re-queries the DOM on every retry, it doesn't hold onto a "stale" reference to a deleted element.
Worked Example: Async Success Message
Imagine a scenario where clicking a "Submit" button triggers an API call. A loading spinner appears for 1.5 seconds, then is replaced by a success message.
// Run this in a Cypress test file (e.g., submission.cy.js)
// Required: Cypress 12.x or newer
it('should display success message after async load', () => {
cy.visit('/form-page');
// 1. Click the submit button
cy.get('#submit-btn').click();
// 2. Assert the success message is visible
// Cypress will retry this query automatically for up to 4s
// It will fail while the spinner is active and pass once the message appears
cy.get('.success-banner').should('be.visible').and('contain', 'Thank you!');
});
In this example, you do not need to know exactly how long the API call takes. Cypress will poll the DOM and proceed the millisecond becomes visible.
Limitations and Trade-offs
Automatic waiting is powerful, but it is not a universal solution for all asynchronous behavior:
- Non-DOM Async: Cypress retries commands that affect the DOM. It does not retry standard JavaScript promises,
setTimeoutcallbacks, or internal application state changes that don't result in a UI update. For these, you may needcy.clock()orcy.intercept()to manage timing. - Negative Assertions: When you assert that an element should not exist (e.g.,
.should('not.exist')), Cypress will wait the full timeout if the element is present, as it hopes the element will disappear. If you need to check for absence immediately, you can pass a custom timeout:cy.get('.popup', { timeout: 0 }).should('not.exist').
Verification and Practical Checks
To confirm that automatic waiting is working in your project rather than a lucky timing coincidence:
- Use the Command Log: Open the Cypress Test Runner. When a command is retrying, you will see the command in the left-hand log flashing or updating its status until it turns green.
- Simulate Slowness: Use Chrome DevTools to throttle your network to "Slow 3G." If your test still passes without manual
cy.wait()calls, the retry-ability is handling the latency. - Compare Execution: Run a test with a known 2-second delay. Compare the total time of a test using automatic waiting versus one using
cy.wait(2000). The automatic version will often finish faster because it proceeds the instant the element appears.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.