The most abused Cypress command ever: cy.wait(TIME)
Tags: cypress, testing, automation, qa
Published: 2024-05-05 23:34:26

**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?

&nbsp;  
##### Timeout override option

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

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

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:

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

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._

&nbsp;  
##### Wait for Network Requests

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

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

// Rest of the test
// [...]
```

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:

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

// Rest of the test
// [...]
```

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.

&nbsp;  
##### 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:

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

// Rest of the test
// [...]
```

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:

``` javascript
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
// [...]
```

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):

``` javascript
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
// [...]
```

&nbsp;  
##### 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:

``` javascript
cy.get('[data-cy="goto-dashboard"]').click()
cy.url().should('include', '/dashboard')
```

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.

&nbsp;  
##### 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.

``` javascript
// 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');
```

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

&nbsp;  
##### 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.

``` javascript
Cypress.Commands.add('waitForCondition', (conditionFn) => {
  const pollCondition = () => {
    if (conditionFn()) {
      return
    }
    cy.wait(100).then(pollCondition)
  }
  pollCondition()
})
```

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

&nbsp;  
##### 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:

``` javascript
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');
});
```

- **Using Plugins**: There are plugins available that extend Cypress's capabilities, such as [cypress-wait-until](https://github.com/NoriSte/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:

``` javascript
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');
  });
});

```

- **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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g5hwd68etcpoxesegnwr.png)

&nbsp;  

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