lecture 1 of 150 completed

How retrying really works

Cypress retries only the last command before your check. Once you know this, confusing failures start to make sense.

The first time this happened to me, I was sure Cypress had a bug. My test failed, I opened the video, and the button was clearly disabled on the screen. It took me an hour to understand what was really going on.

Let's start with that test.

cy.get('[data-testid="save-contact"]').click().should('be.disabled');

You click Save. The button becomes disabled while the app saves. So this should pass. But sometimes it fails, and the error says the button is not disabled.

To understand why, you need to know what Cypress retries.

Cypress only retries the last command before your check.

Look at a simple example first:

cy.get('[data-testid="contact-row"]').should('have.length', 1);

Here is what Cypress does:

  1. It runs cy.get and finds the rows.
  2. It checks the count.
  3. If the count is wrong, it runs cy.get again.
  4. It repeats until the count is 1, or until time runs out.

So the search and the check repeat together. That is why this test survives a slow page.

Now back to our broken test. .click() is an action. A command that changes the page, like click, type, or check. Cypress will never repeat an action. Imagine if it did: your test would click Save five times and create five contacts.

So Cypress clicks once, then checks if the button is disabled. If the app needs 50 milliseconds to disable it, the check already failed.

The fix is simple. Split it into two lines:

cy.get('[data-testid="save-contact"]').click();
cy.get('[data-testid="save-contact"]').should('be.disabled');

Now the second cy.get runs again and again until the button is disabled. The action happens once. The check retries. That is what you want.

There is one more case you will meet. Look at this:

cy.get('[data-testid="contacts-table"]')
  .find('[data-testid="contact-row"]')
  .should('have.length', 1);

Cypress retries .find(), but not cy.get. Usually that is fine. But if the app replaces the whole table while loading, cy.get is holding an old table that no longer exists. .find() keeps searching inside it and finds nothing. You get a strange error about an element you can see with your own eyes.

If that happens, put the search and the check together:

cy.get('[data-testid="contact-row"]').should('have.length', 1);

Remember this: never attach a check to a click or a type. Keep your search right before your check, on its own line.

Reference: Retry-ability.