Cypress: How to handle waiting for elements with click handlers
Asked Answered
B

2

8

The code I’m testing has a lot of <a> tag elements that render with an href tag but shortly after page load are given some click event that does something different (such as opening a modal). The href is a fallback and the intended behavior is in the click event.

Cypress is often too fast for the page’s javascript and clicks the element before the event has been added to it. This causes the page to navigate to the default href, rather than triggering the behavior I want to test. Here’s an example, where I use a timeout to simulate the slow-loading JS:

<a id="reveal_cats" href="https://http.cat" >
  Show me cats!
</a>

<div id="cats_div" style="display: none;">
  Cats!
</div>

<script>
  // Using a timeout to simulate a slow-loading JS file
  // that adds a click handler
  setTimeout(
    () => {
      console.log("Handler is added")
      $("#reveal_cats").on("click", function(e) {
        e.preventDefault();
        $("#cats_div").show()
      })
    }, 3000
  )
</script>
it("fails because the click handler isn't loaded yet", () => {
  cy.contains("a", "Show me cats!").click()
  // This fails because the event handler isn't loaded yet
  // so instead we've navigated to http.cat
  cy.get("#cats_div").should("be.visible")
})
it("passes, but uses an undesirably long hardcoded wait", () => {
  cy.contains("a", "Show me cats!").click()
  cy.wait(5000)
  cy.get("#cats_div").should("be.visible")
})

How can I get Cypress to wait for the handler to be loaded?

My first guess was to use an intercept to wait for the JS file that adds the handler, but the filenames are randomly generated, so they’re not reliable.

My second guess was to try to assert for event handlers on the element, but I don’t see a standard assertion for that. I thought maybe I could look for listeners in a then() block using the jQuery utility, but I don’t believe that will retry properly if the event isn’t found on the first try.

The only solution I’ve found so far that works is hardcoded waits…

Babblement answered 2/11, 2023 at 20:55 Comment(0)
C
8

The easiest way to wait for the click handler is to use the cypress-cdp plugin.

This is the test for your example page:

import 'cypress-cdp'

it('waiting for click event handler on link element', () => {
  cy.visit('cats.html')

  cy.get('#cats_div').should('not.be.visible')    // evaluates immediately

  cy.hasEventListeners('#reveal_cats', { type: 'click' })
  cy.get('#reveal_cats').click()

  cy.get('#cats_div', {timeout: 100})  // setting really short timeout for demo 
    .should('be.visible')
    .and('contain', 'Cats!')
})

enter image description here


Composing a chained version

cy.hasEventListeners() is a parent command, so it's not designed to use chaining like cy.get().find().hasEventListeners().

Internally it uses cy.get():

// plugin source

Cypress.Commands.add('hasEventListeners', (selector, options = {}) => {
  ...
  cy.get(selector, { log: false })

Therefore, you could apply chaining using .within() and the internal cy.get() will be restricted to the previous chained subject.

cy.get('x').find('y')   // "root" is now 'y'
  .within(() => {
    cy.hasEventListeners('#reveal_cats', { type: 'click' })
  })

Or you could compose your own command

Cypress.Commands.add('hasEventListeners2', {prevSubject: true}, (subject, options) => {
  const selector = subject.selector
  cy.hasEventListeners(selector, options)
})

cy.get('div').find('#reveal_cats').hasEventListeners2({ type: 'click' })
Commissionaire answered 3/11, 2023 at 7:18 Comment(2)
I've accepted this answer since it solves the problem posed in my question. Unfortunately, it likely won't work for my actual use case since often I need to chain the hasEventListeners command off a different Cypress command, such as cy.contains() or cy.get().find(), and it doesn't appear that the package supports that. Still, helpful!Babblement
I've added a couple of approaches to that problem.Commissionaire
D
0

Try this:

Cypress configuration - timeouts

It will wait until the Element becomes visible within the specified timeout period.

You can set the default in cypress.config.js or set it directly in the element.

Ex.


cy.get("#cats_div",{timeout:20000}).should("be.visible")

It will wait for the element to be displayed for up to 20 seconds.

Dormer answered 3/11, 2023 at 2:43 Comment(2)
This isn't the problem I'm trying to solve. The problem isn't that the element takes too long to appear. It's that it never appears, since the click handler isn't attached to the element when Cypress clicks it.Babblement
Using timeout in cypress specs is almost always a bad practice - either you have to set it to high number (like in this example) resulting in slow tests or set it to smaller value and risk flaky test (sometimes it will pass, sometimes not, often depending on cpu load or network speed)Unseal

© 2022 - 2025 — McMap. All rights reserved.