DEV Community

Cover image for The most abused Cypress command ever: cy.wait(TIME)
Sebastian Clavijo Suero
Sebastian Clavijo Suero

Posted on • Updated on

The most abused Cypress command ever: cy.wait(TIME)

The inexorable(?) ticking TIME bomb!


ACT 1: Exposition

How often have you seen a cy.wait(5000) in a peer's pull request, or a cy.wait(10000) in a stubbornly flaky test that someone asked you to review because it's not always passing?

I'd wager 0.0005 gold tokens of membership (approximately $1) that it occurs to you several times in a week!

Why is cy.wait(TIME) labeled as an anti-pattern in testing, yet it's frequently used by so many QA engineers and testers, even if only occasionally?

There is an easy answer: it is convenient. Very convenient! But at what cost?


ACT 2: Confrontation

In my opinion, the cost can be considerably high:

  1. It introduces unnecessary delays, making your tests slower.

  2. It introduces dangerous uncertainty, which can make your tests unpredictable and, in other words, FLAKY—a term dreaded by testers.

However, there's no need to despair; we have several alternatives to the notorious cy.wait(TIME) at our disposal. These options can be added to your everyday toolkit and are definitely more deterministic strategies for synchronizing your tests with the application's state.

But what are these alternatives?

 

Timeout override option

Below is one of the most common "patterns" I have observed in test automation:

cy.get('[data-cy="calculate-totals"]').click();
cy.wait(10000);
cy.get('[data-cy="total-value"]').should('be.equal', '$125');
Enter fullscreen mode Exit fullscreen mode

The cy.wait(10000) command suggests that the test waits for 10 seconds after clicking the calculate-totals button to allow time for an operation, such as an asynchronous calculation or data retrieval, to complete before it checks the result. By default, Cypress will fail an assertion if the 4-second default timeout is exceeded.

However, this code will always wait the full 10 seconds before checking the assertion, even if the calculation takes less time.

A more efficient approach would be to use a custom timeout for this specific assertion, accommodating potentially lengthy calculations:

cy.get('[data-cy="calculate-totals"]').click();
cy.get('[data-cy="total-value"]', { timeout: 10000).should('be.equal', '$125');
Enter fullscreen mode Exit fullscreen mode

With this simple change, the test will proceed as soon as the calculation is finished, without unnecessarily waiting the full 10 seconds, or it will fail if the 10-second timeout is reached.

You can override the default pageLoadTimeout and defaultCommandTimeout for all your tests in the cypress.config.js file.

 

Wait for Network Requests

How many times have you come across code like this in tests?

cy.visit('/page-that-loads-data');
cy.wait(5000);

// Rest of the test
// [...]
Enter fullscreen mode Exit fullscreen mode

You probably wouldn't be exaggerating if you said hundreds of times!

In this example, cy.wait(5000) forces the test to pause for 5 seconds after visiting the page, hoping that this will be enough time for the data of the page to load.

Using fixed wait times like this is not advised as it can result in unreliable tests and longer test times, especially if the actual loading time differs from the wait period.

However, if you take a bit more time to analyze how the application behaves, you might notice, for example, that the page displays data returned from an API call to /api/data. In such scenarios, we can utilize cy.intercept() and cy.wait(ALIAS) to our advantage:

cy.intercept('GET', '/api/data').as('getData');
cy.visit('/page-that-loads-data');
cy.wait('@getData');

// Rest of the test
// [...]
Enter fullscreen mode Exit fullscreen mode

This Cypress code performs the following actions:

  • It sets up an interceptor to monitor a GET request to the /api/data endpoint, and once such a request occurs assigns it the alias getData.

  • It navigates to the /page-that-loads-data URL, which should trigger the monitored GET request.

  • It pauses the test execution until the aliased getData request is completed.

The use of cy.wait('@getData') helps create a more reliable test by ensuring that subsequent commands only execute after the necessary data has been retrieved.

Note that cy.wait() also supports the timeout override option, allowing for custom timeout settings.

 

Wait for DOM Elements on page load

Static wait times often suggest that the test is not properly synchronized with the application's state.

This is a "pattern" you might often encounter in Cypress tests:

cy.visit('/page-that-loads-data');
cy.wait(5000)

// Rest of the test
// [...]
Enter fullscreen mode Exit fullscreen mode

In this example, the test navigates to a page using cy.visit() and then pauses for a fixed duration of 5 seconds, assuming this will allow enough time for the page to fully load.

Instead of relying on cy.wait(TIME), it's better to use cy.get() with assertions that automatically retry until the presence of specific DOM elements signals the page has loaded. These assertions can be chained to keep retrying until certain conditions are met.

For instance:

cy.visit('/page-that-loads-data');
// Assert data is visible and has 5 elements
cy.get('[data-cy="data-list"]')
    .should('be.visible')
    .and('have.length', 5)

// Rest of the test
// [...]
Enter fullscreen mode Exit fullscreen mode

In this case, we expect the page to be fully loaded once the DOM element with the data-cy="data-list" attribute is visible and contains 5 items. Cypress will continue retrying this assertion until it passes or the default timeout is reached.

For pages that may load slowly, consider using this technique with an increased timeout (remember, the default Cypress timeout is only 4 seconds):

cy.visit('/page-that-loads-data');
// Assert data is visible and has 5 elements
cy.get('[data-cy="data-list"]', { timeout: 10000 })
    .should('be.visible')
    .and('have.length', 5);

// Rest of the test
// [...]
Enter fullscreen mode Exit fullscreen mode

 

Wait for Routes

When testing a single-page application (SPA) and waiting for route changes, cy.url() can be used to wait for the URL to update:

cy.get('[data-cy="goto-dashboard"]').click()
cy.url().should('include', '/dashboard')
Enter fullscreen mode Exit fullscreen mode

Keep in mind that cy.url() is a Cypress command that retries automatically until the assertion is met or a timeout occurs. Additionally, cy.url() supports the timeout override option, allowing for custom timeout settings.

 

Wait for Animations

If animations or transitions affect the test flow, consider disabling them in your test environment or use Cypress commands to wait for the animation to complete.

// Trigger the animation
cy.get('#animated-element').click();

// Wait for the animation to complete by checking for a CSS property that indicates completion
// For example, if the animation ends with the element being fully opaque, you could check the 'opacity' property
cy.get('#animated-element').should('have.css', 'opacity', '1');
Enter fullscreen mode Exit fullscreen mode

This example assumes the end state of the animation can be detected by a change in a CSS property.

 

Custom Wait Commands

If you have a specific condition to wait for, you can create a custom command that uses recursion and cy.wait() with a short delay to poll for the condition.

Cypress.Commands.add('waitForCondition', (conditionFn) => {
  const pollCondition = () => {
    if (conditionFn()) {
      return
    }
    cy.wait(100).then(pollCondition)
  }
  pollCondition()
})
Enter fullscreen mode Exit fullscreen mode

I must say that this is by far my least favorite alternative!

 

Anything else?

We discussed a comprehensive list of alternatives to cy.wait(TIME) for multiple scenarios in Cypress testing. But here are a few more techniques that can be considered:

  • Alias Waiting: Besides waiting for network requests, you can alias almost any command in Cypress with .as() and then wait for it with cy.wait().

  • Page Event Listeners: Sometimes, applications emit custom events when certain actions are completed. Cypress itself does not directly provide a mechanism to listen for custom events on the page as part of its API. However, you can use standard JavaScript within a Cypress test to listen for custom events emitted by your application.

    Let's say your application emits a custom event called dataLoaded on the window object when data has finished loading. You can create a Cypress test that waits for this event before proceeding:

cy.visit('/page-that-emits-event');

// Create a promise that resolves when the `dataLoaded` event is fired
const waitForDataLoadedEvent = new Cypress.Promise((resolve, reject) => {
  cy.window().then((win) => {
    win.addEventListener('dataLoaded', resolve);
  });
});

// Use the `cy.then()` command to wait for the promise to resolve
cy.then(() => waitForDataLoadedEvent).then(() => {
  // The `dataLoaded` event has been fired, and we can continue with our assertions
  cy.get('#data-container').should('contain', 'Data loaded');
});
Enter fullscreen mode Exit fullscreen mode
  • Using Plugins: There are plugins available that extend Cypress's capabilities, such as cypress-wait-until by Stefano Magni, which can be used to wait for a certain condition to be true before proceeding.

    Here is an example of how to use cy.waitUntil() in a Cypress test:

describe('Example using cypress-wait-until', () => {
  it('waits for a specific condition to be true', () => {
    cy.visit('/page-with-dynamic-content');

    // Use cy.waitUntil to wait for a condition to be true
    cy.waitUntil(() => 
      // In this case, we're waiting for an element with attribute [data-cy="dynamic-element"] to contain the text 'Loaded'
      cy.get('[data-cy="dynamic-element"]').then($el => $el.text() === 'Loaded'),
      {
        errorMsg: 'The element did not load in time', // Custom error message
        timeout: 10000, // Timeout after which the waitUntil will fail
        interval: 500 // Time to wait between the retries
      }
    );

    // Continue with other actions or assertions after the condition is met
    cy.get([data-cy="dynamic-element"]).should('be.visible');
  });
});

Enter fullscreen mode Exit fullscreen mode
  • Conditional Testing: Sometimes, you may need to perform actions based on the state of the application. Cypress allows you to use .then() to add custom logic that can decide what to do next based on the current state of the DOM or any other condition.

  • Cypress Clock and Ticking: For controlling time-based functions like setTimeout or setInterval, Cypress provides the cy.clock() and cy.tick() commands, which can be used to test time-dependent code without real waiting.

  • Stubbing and Mocking: When you don't need to test the actual network requests, you can stub or mock them to instantly respond with predefined data, eliminating the need to wait for real network requests to complete.


ACT3: Resolution

By implementing these strategies in your Cypress tests, you can enhance their robustness and reliability. Instead of depending on arbitrary wait times—which can lead to flaky tests and increased execution time—you align your tests with the actual behavior of the application.

This synchronization ensures that your tests wait for specific events, such as the completion of network requests, animations, or the appearance of elements, before proceeding. As a result, your tests are less prone to failure due to timing issues and provide more accurate results.

By leveraging Cypress's built-in commands for dynamic waiting, such as cy.get() with custom timeouts and cy.intercept() for network requests, you create a more efficient and stable testing environment that can adapt to the varying response times of your application.

I believe that these tools can be highly effective at defusing the now not inexorable ticking TIME bomb introduced by the use of cy.wait(TIME) in your automated tests.

Image description

 

Don't forget to subscribe, leave a comment, or give a thumbs up if you found this post useful.
Happy reading!

Top comments (11)

Collapse
 
jasonstitt profile image
Jason Stitt

Not sure which is worse, the test suite taking forever to run or the arbitrary interval that may not be correct. Thanks for sharing.

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

I would say definitely the the application under test that takes that much time to show results to the user. :)

Collapse
 
jasonstitt profile image
Jason Stitt

Good point!

Collapse
 
joydeep100 profile image
Joydeep D

The most comprehensive article on wait management. Great one.

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero • Edited

Thank you very much @joydeep100 ! 🙌

Collapse
 
marktnoonan profile image
Mark Noonan

Nicely written, lots of information, really useful examples, but an approachable style 💯

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

Thank you @marktnoonan , really appreciated!

Collapse
 
mcondon profile image
Micah Condon

Nice! There is almost always a better option than wait, if you understand exactly what outcome you're really waiting for

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

Thank you Micah. Glad you found it insightful.

Collapse
 
walmyrlimaesilv profile image
Walmyr

Excellent blog post! Thanks for sharing it.

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

@walmyrlimaesilv thank you very much! 🤲