Element Does Not Appear

Imagine the page is loading and might flash a quick error message. In most cases, the message is not there, but sometimes it quickly comes and goes. Even if the message stays, how do you confirm that an element does not exist? If you simply check the existence, the check quickly resolves before the element is there

// such check would not help if it runs
// before the error element shows up
cy.get('.error').should('not.exist')

What we need is a custom command or assertion that keeps checking the DOM for a period of time. Once it sees an element that we are looking for, the test should immediately fail. We can write such test using cy.document and cy.then combination.

<div id="main">
  <p id="message">Loading...</p>
</div>

The application is loading the "data", but there might be an error. If there is an error, a message is shown on the screen, and it "retries" loading the data. So we could potentially see a flash "Error" on the page. How do we detect this?

setTimeout(() => {
  document.getElementById('message').innerText = 'Finished'
}, 2000)
// a possible error message
// set the right side to 1 to always have the error element
if (Math.random() < 0) {
  setTimeout(() => {
    document.getElementById('main').innerHTML +=
      '<p class="error" id="error">Got an error, retrying</p>'
    setTimeout(() => {
      // remove the error element
      const error = document.getElementById('error')
      error.parentNode.removeChild(error)
    }, 300)
  }, 1000)
}

We can "ping" the DOM using a "while" loop. To terminate the loop, we can use the comment timeout and finding the DOM element "#message" with specific text. The simplest solution would be a simple asynchronous while loop

cy.document().then(async (doc) => {
  // try for 2 seconds
  const started = Date.now()
  while (Date.now() - started < 2_000) {
    const gameHistoryTable = doc.querySelector('.error')
    if (gameHistoryTable) {
      throw new Error('The error element was found')
    }
    // sleep and try again
    await Cypress.Promise.delay(delay)
  }
})

The above solution works. Let's extend it to make it more generic. Let's add a cy.log and refactor the selectors and timeouts:

cy.contains('#message', 'Loading...')
// confirm the ".error" element NEVER shows up during 2 seconds
// if we find the element "#message" with text "Finished" we stop checking
cy.document().then((doc) => {
  const started = Date.now()
  const selector = '.error'
  // check the DOM every N milliseconds
  const delay = 50
  // maximum timeout while checking the DOM
  const timeout =
    Cypress.config('defaultCommandTimeout') || 2_000
  cy.log(
    `checking if ${selector} appears within ${timeout}ms`,
  ).then(async () => {
    while (Date.now() - started < timeout - delay) {
      const gameHistoryTable = doc.querySelector(selector)
      if (gameHistoryTable) {
        throw new Error(`Element ${selector} was found`)
      }
      const finished = doc.querySelector('#message')
      if (finished.innerText.includes('Finished')) {
        // all done
        break
      }
      await Cypress.Promise.delay(delay)
    }
  })
})

cy.contains('#message', 'Finished')

Note: my plugin cypress-mapopen in new window includes the command cy.never(selector) that implements the solution from this recipe.