Retry-ability

For more information see Cypress retry-ability guideopen in new window.

Element added to the DOM

Let's test a situation where the application inserts a new element to the DOM

<div id="app-example"></div>
<script>
  setTimeout(() => {
    document.getElementById('app-example').innerHTML =
      '<div id="added">Hello</div>'
  }, 2000)
</script>

Because Cypress querying commands have the built-in existence check, all we need to do is to ask:

// cy.get will retry until it finds at least one element
// matching the selector
cy.get('#added')

Element becomes visible

If an element is already hidden in the DOM and becomes visible, we can retry finding the element by adding a visibility assertion

<div id="app-example">
  <div id="loader" style="display:none">Loaded</div>
</div>
<script>
  setTimeout(() => {
    document.getElementById('loader').style.display = 'block'
  }, 2000)
</script>

Because Cypress querying commands have the built-in existence check, all we need to do is to ask:

// the cy.get command retries until it finds at least one visible element
// matching the selector
cy.get('#loader').should('be.visible')

Element becomes visible using jQuery :visible selector

You can optimize checking if an element is visible by using the jQuery :visible selector

<div id="app-example">
  <div id="loader" style="display:none">Loaded</div>
</div>
<script>
  setTimeout(() => {
    document.getElementById('loader').style.display = 'block'
  }, 2000)
</script>
// cy.get has a built-in existence assertion
cy.get('#loader:visible').should('have.text', 'Loaded')
// or use a single cy.contains command
cy.contains('#loader:visible', 'Loaded')

Matching element's text

Imagine the element changes its text after two seconds. We can chain cy.getopen in new window and cy.invokeopen in new window commands to get the text and then use the match assertion to compare the text against a regular expression.

<div id="example">loading...</div>
<script>
  setTimeout(() => {
    document.getElementById('example').innerText = 'Ready'
  }, 2000)
</script>

Notice that .invoke('text') can be safely retried until the assertion passes or times out.

cy.get('#example')
  .invoke('text')
  .should('match', /(Ready|Started)/)

Rather than splitting cy.get + cy.invoke commands, let's have a single command to find the element and match its text using the cy.containsopen in new window command.

// equivalent assertion using cy.contains
// https://on.cypress.io/contains
cy.contains('#example', /(Ready|Started)/)

Multiple assertions

<div id="example"></div>
<script>
  setTimeout(() => {
    document.getElementById('example').innerHTML = `
      <button id="inner">Submit</button>
    `
  }, 2000)
  setTimeout(() => {
    document
      .getElementById('inner')
      .setAttribute('style', 'color: red')
  }, 3000)
</script>
cy.get('#inner')
  // automatically waits for the button with text "Submit" to appear
  .should('have.text', 'Submit')
  // retries getting the element with ID "inner"
  // until finds one with the red CSS color
  .and('have.css', 'color', 'rgb(255, 0, 0)')
  .click()

Counts retries

One can even count how many times the command and assertion were retried by providing a dummy .should(cb) function. A similar approach was described in the blog post When Can The Test Click?open in new window.

<div id="red-example">Will turn red</div>
<script>
  setTimeout(() => {
    document
      .getElementById('red-example')
      .setAttribute('style', 'color: red')
  }, 800)
</script>
let count = 0
cy.get('#red-example')
  .should(() => {
    // this assertion callback only
    // increments the number of times
    // the command and assertions were retried
    count += 1
  })
  .and('have.css', 'color', 'rgb(255, 0, 0)')
  .then(() => {
    cy.log(`retried **${count}** times`)
  })

Merging queries

Instead of splitting querying commands like cy.get(...).first() use a single cy.get with a combined CSS querty using the :first selector.

<ul id="items">
  <li>Apples</li>
</ul>
<script>
  setTimeout(() => {
    // notice that we re-render the entire list
    // and insert the item at the first position
    document.getElementById('items').innerHTML = `
      <li>Grapes</li>
      <li>Apples</li>
    `
  }, 2000)
</script>

How do we confirm that the first element in the list is "Grapes"? By using a single cy.get command.

cy.get('#items li:first').should('have.text', 'Grapes')
// equivalent
cy.contains('#items li:first', 'Grapes')

Query the element again

<div id="cart">
  <div>Apples <input type="text" value="10" /></div>
  <div>Pears <input type="text" value="6" /></div>
  <div>Grapes <input type="text" value="5" /></div>
</div>
cy.get('#cart input') // query command
  .eq(2) // query command
  .clear() // action command
  .type('20') // action command

The above test is ok, but if you find it to be flaky, add more assertions and query the element again to ensure you find it even if it re-rendered on the page.

// merge the cy.get + cy.eq into a single query
const selector = '#cart input:nth(2)'
cy.get(selector).clear()
// query the input again to make sure has been cleared
cy.get(selector).should('have.value', '')
// type the new value and check
cy.get(selector).type('20')
cy.get(selector).should('have.value', '20')

Watch the explanation for the above test refactoring in my video Query Elements With Retry-Ability To Avoid Flakeopen in new window.

Element appears then loads text

All assertions attached to the querying command should pass with the same subject.

📺 Watch this example explained in the video Element Becomes Visible And Then Loads Textopen in new window.

<div id="app-example">
  <div id="loader" style="display:none">Loaded</div>
</div>
<script>
  setTimeout(() => {
    document.getElementById('loader').style.display = 'block'
  }, 2000)
  setTimeout(() => {
    document.getElementById('loader').innerText =
      'Username is Joe'
  }, 4050)
</script>

Notice that the element becomes visible after 2 seconds, well within the default command timeout of 4 seconds. But it gets the expected text slightly after 4 seconds. Both assertions must pass together, thus the following test fails.

cy.get('#loader')
  .should('be.visible')
  .and('have.text', 'Username is Joe')

One solution is to increase the timeout in cy.get command

cy.get('#loader', { timeout: 5_000 })
  .should('be.visible')
  .and('have.text', 'Username is Joe')

Alternative: split the assertions by inserting another cy.get element command to "restart" the timeout clock.

cy.get('#loader').should('be.visible')
cy.get('#loader').should('have.text', 'Username is Joe')

Item is added to the local storage

Let's confirm the application sets the productId in the localStorage object. Unfortunately, we do not know when the application is going to set it, only that it will be within a couple of seconds after clicking the button "Save".

<button id="save">💾 Save</button>
<script>
  document
    .getElementById('save')
    .addEventListener('click', () => {
      setTimeout(() => {
        window.localStorage.setItem('productId', '1234abc')
      }, 1500)
    })
</script>
cy.contains('button', 'Save').click()
cy.window() // query
  .its('localStorage') // query
  .invoke('getItem', 'productId') // query
  .should('exist') // assertion
  .then(console.log) // command
  .should('match', /^\d{4}/) // assertion

By inserting an assertion should('exist') after queries, we retry checking the local storage until the item is found. Then we can use other commands, like cy.then(console.log) that do not retry.

Fun: call function using retry-ability

📺 Watch the explanation for these recipes in Fun With Cypress Query Commands And Asynchronous Functionsopen in new window.

Usually we have data subject passing through Cypress queries and functions until the assertions pass. For example, the subject could be an object and its property value:

// use max 10 but for demos use something larger like 50
// to show off retries
const getRandomN = () => Cypress._.random(10, false)
const o = {}
const i = setInterval(() => (o.n = getRandomN()), 0)

cy.log('**subject is an object**')
cy.wrap(o) // subject is an object {n: ...}
  .its('n') // subject is a number
  .should('equal', 7)

We can flip it around and wrap the function getRandomN itself as Cypress command chain subject. It will sit there doing nothing until we call it. Tip: you can invoke any function using Function.prototype.call or Function.prototype.apply methods.

cy.log('**subject is a function**')
cy.wrap(getRandomN) // subject is function "getRandomN"
  .invoke('call') // subject is a number
  .should('equal', 7)

The above chain of Cypress queries retries calling getRandomN.call as quickly as it can until the assertion passes.

😒 Unfortunately, cy.invoke query command does not yield the resolved value from asynchronous functions, so you cannot write retry-able asynchronous chains, like you can do using cypress-recurseopen in new window plugin.

// 🚨 DOES NOT WORK
cy.wrap(fetch)
  .invoke('call', null, '/get-n')
  .invoke('json')
  .its('n')
  .should('equal', 7)

More fun: invoke an asynchronous function with retry-ability

If you look at the above example, maybe you think it is impossible to make cy.invoke wait for the resolved value. Do not worry, life finds a way. Here is super hacky way to retry fetching data from an external endpoint using fetch, cy.invoke, and built-in retries

First, I will add a query named nsync to implement polling.

Cypress.Commands.addQuery('nsync', () => {
  let value
  return (subject) => {
    if (typeof subject === 'object' && 'then' in subject) {
      subject.then((x) => (value = x))
      const result = value
      value = undefined
      return result
    } else {
      return subject
    }
  }
})

After each asynchronous function call, like fetch or res.json, add nsync() query to "synchronize" it.

cy.wrap(fetch)
  .invoke('call', null, 'http://localhost:4200/random-digit', {
    headers: { 'x-delay': 100 },
  })
  .nsync()
  .invoke('json')
  .nsync()
  .its('n')
  .should('equal', 7)