Shrink The Time Gap

How a Cypress test can confirm there are no unexpected elements.

The application

Imagine a very simple application:

  1. you click a button
  2. the application does something while showing the text "Loading"
  3. the application shows the success message "Loaded"

Simple. If everything goes well, the end-to-end test can be

1
2
3
4
5
cy.contains('button', 'load')
.click()
.should('have.text', 'Loading...')
// confirm the final state
cy.contains('Loaded')

🎁 You can find the example application and the tests in my collection of Cypress examples.

Everything goes well. We do not know when the "loading..." message switches to "Loaded", but the test relies on Cypress retry-ability to keep checking the page until it find the text "Loaded"

1
2
3
// even if it takes 3.9 seconds
// this test snippet keeps checking the page
cy.contains('Loaded')

If the application might take 10 seconds to show the "Loaded" message, we can increase the retry-ability timeout:

1
2
3
// keep trying to find the text "Loaded" on the page
// until it is found or 10 seconds pass
cy.contains('Loaded', { timeout: 10_000 })

The most common situation for adding a timeout parameter to a Cypress command like cy.contains is to increase the time limit. But there are cases when we want to shrink the timeout period to zero or just a few milliseconds instead.

Unexpected elements

Imagine that our application instead of successfully loading the data, for some reason shows an unexpected warning message, and then completes successfully.

Hmm, notice the test still passed. We do not care about the unexpected "Something went wrong" message, we are looking for the "Loaded" text, and it eventually appear. How do we "catch" the expected elements? We cannot use negative assertions because A) we do not know all possible unexpected elements and B) we cannot control the timing when these elements appear to know when to check. Here is an updated test that still does not "notice" the warning:

1
2
3
4
5
6
7
8
9
// 🚨 DOES NOT WORK
// will NOT catch the warning message that clearly appears
cy.contains('button', 'load')
.click()
.should('have.text', 'Loading...')
// the warning message should not be there
cy.contains('Something went wrong').should('not.exist')
// confirm the final state
cy.contains('Loaded')

We need something better. We need to really "shrink" the time between the "Loading..." element going away and the "Loaded" text appearing. Then any other element trying to sneak in will be caught.

Shrink the time gap using the timeout option

Here is what we can do. We can confirm when the "Loading..." text goes away.

1
2
3
4
5
cy.contains('button', 'load')
.click()
.should('have.text', 'Loading...')
// the "Loading..." element goes away
cy.contains('button', 'Loading...').should('not.exist')

Note: despite having "not.exist", cy.contains('button', 'Loading...').should('not.exist') is an example of a positive assertion. We check that something happens in response to an action.

Ok, so we know the "Loading..." text is gone. Now we are looking to see the "Loaded" text there immediately.

1
2
3
4
// the "Loading..." element goes away
cy.contains('button', 'Loading...').should('not.exist')
// and the final state appears very quickly
cy.contains('Loaded', { timeout: 0 })

Beautiful - if there are no extra elements, the test passes. Now let's see what happens when a warning message does "insert" itself.

By setting the timeout: 0 we tell Cypress to really shrink the retries limit to make sure no unexpected element can sneak in.

Video

You can watch this blog post explained in the video below

See also