Why is Cypress saying my element is detached after just running a get command?

Viewed 32504

Objective: I want to click on a particular element on the page using an accessibility selector with cypress

Code

cy.findAllByRole('rowheader').eq(2).click();

Error

Timed out retrying: cy.click() failed because this element is detached from the DOM.

<th scope="row" data-automation-id="taskItem" aria-invalid="false" tabindex="-1" class="css-5xw9jq">...</th>

Cypress requires elements be attached in the DOM to interact with them.

The previous command that ran was:

  > cy.eq()

This DOM element likely became detached somewhere between the previous and current command.

Question: I can see in the DOM that this element is still present - there is no logic that will detach this element from the DOM, and the eq method certainly wouldn't do that. Additionally, the findAllByRow method is clearly working as it has found the correct th element I want to click on. How come its saying the element is detached? Is there a workaround for this situation?

11 Answers

This could be bad advice, but can you try the following?

cy.findAllByRole('rowheader').eq(2).click({force: true})

Without a reproducible example, it's speculative, but try adding a guard before the click using Cypress.dom.isDetached

cy.findAllByRole('rowheader').eq(2)
  .should($el => {
    expect(Cypress.dom.isDetached($el)).to.eq(false)
  })   
  .click()

If the expect fails, then a prior line is causing the detach and cy.findAllByRole is not re-querying the element correctly.

If so you might be able to substitute a plain cy.get().


I also like @AntonyFuentesArtavia idea of using an alias, because the alias mechanism saves the original query and specifically requeries the DOM when it finds it's subject is detached.

See my answer here

From the Cypress source

const resolveAlias = () => {
  // if this is a DOM element
  if ($dom.isElement(subject)) {
    let replayFrom = false

    const replay = () => {
      cy.replayCommandsFrom(command)

      // its important to return undefined
      // here else we trick cypress into thinking
      // we have a promise violation
      return undefined
    }

    // if we're missing any element
    // within our subject then filter out
    // anything not currently in the DOM
    if ($dom.isDetached(subject)) {
      subject = subject.filter((index, el) => $dom.isAttached(el))

      // if we have nothing left
      // just go replay the commands
      if (!subject.length) {
        return replay()
      }
    }

The answer to your question is written in the error message that you got:

Timed out retrying: cy.click() failed because this element is detached from the DOM.

...

Cypress requires elements be attached in the DOM to interact with them.

The previous command that ran was:

cy.eq()

This DOM element likely became detached somewhere between the previous and current command.

Getting this error means you've tried to interact with a "dead" DOM element - meaning it's been detached or completely removed from the DOM. So by the time, cypress is about to click the element eq() was either detached or removed from the DOM.

In modern JavaScript frameworks, DOM elements are regularly re-rendered - meaning that the old element is thrown away and a new one is put in its place. Because this happens so fast, it may appear as if nothing has visibly changed to the user. But if you are in the middle of executing test commands, it's possible the element you're interacting with has become "dead". To deal with this situation you must:

  • Understand when your application re-renders
  • Re-query for newly added DOM elements
  • Guard Cypress from running commands until a specific condition is met

When we say guard, this usually means:

  • Writing an assertion
  • Waiting on an XHR

You can read more from cypress docs and from the official cypress blog.

Solution: Make sure your elements are loaded and visible first and then perform the click()

cy.findAllByRole('rowheader').eq(2).should('be.visible').click();

I'm running in the same issue, get the same error while running:

cy.get('<elementId>').should('be.visible').click();

I can see as the test runs that it finds the element (and highlights it), the assertion is validated and then somehow the .click() fails to find the element even though it is chained.

I found that adding a static wait before this line for a couple seconds addresses the issue, but I am not sure why I would need to do that and don't really want to use a static wait.

There are no asynchronous tasks running so there is no way to do a dynamic wait.

Here is something that helped me to avoid that detached issue. Using Cypress aliases and the built-in assertion retry-ability.

Example:

cy.get('some locator').first().find('another locator').eq(1).as('element');
cy.get('@element').should(***some assertion to be retried until met***);

So even though I'm traversing a bunch of elements (which could lead to problems because one of the parents could potentially get detached), in the end when I put an alias on top of it, I'm adding a direct link to the final element that was yielded at the end of the chain. Then when I refer to that alias Cypress will re-query the final element and retry it as needed based on any assertions added on top of it. Which indeed helped me a lot to prevent that detached problem.

This happens because React rerenders the whole page. Try to find element with one command:

// do this
cy.get('[role="rowheader"]:nth-ckild(2)').click();

// instead of this
cy.findAllByRole('rowheader').eq(2).should('be.visible').click();

Using a single command fixes the fact that Cypress retries only the last command before the should command. So, previously, only the .eq(2) will be retried. Whereas, you need cy.findAllByRole('rowheader') to be retried too.

Cypress suggests another solution of alternating commands and assertions. It didn't work for me but you can try it:

cy.findAllByRole('rowheader').should('be.visible').eq(2).should('be.visible').click();

Couldn't edit "Ε Г И І И О"'s answer as the edit queue is full, but i think it's important to complement the answer for future learners.

cy.findAllByRole('rowheader').eq(2).click({force: true})

Using { force: true } on click works due to the nature of cypress. According to their documentation the argument force "Forces the action, disables waiting for actionability".

As for "waiting for actionability", it references the assertions section that states:

  • .click() will automatically wait for the element to reach an actionable state
  • .click() will automatically retry until all chained assertions have passed

So this basically disables the above, and that's why it works. One will probably find this useful in some libraries/frameworks that manages the website rendering (like React and Angular).

I am getting the same problem as per the question title during a cypress test, which tries to click on a particular option from a drop-down list.

During the test, it clicks on this drop-down menu element, and then, it tries to get the required option from it.

I applied some asserting suggestions provided here as mentioned below. All of them got passed, but the option from the drop-down list, which I wanted to get clicked wasn't performed during the test.

  • wait(5000)
  • should($item => { expect(Cypress.dom.isDetached($item)).to.equal(false); })
  • should('be.visible')
  • click({ force: true })

Still, the test gives me the same error as before. Any kind assistance here from anyone?

I was getting this error when I was using below statement: cy.wrap($e1).click() Then the below statement worked for me: $e1.click() worked for me without using any wrap method.

The way I solved this was to increase the timeout in the cypress.json file from 4 seconds to 10. add "defaultCommandTimeout": 10000, at the root level of your config object.

This is not a fix, but a workaround. If an element is taking too long to be interactive on your page there's some issue there to be debugged.

Also, make sure you are running your tests across the build and not the dev environment. Build is the compiled code and also the one that your users will see.

Related