52

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?

Gwynn
  • 553
  • 2
  • 5
  • 10
  • As you say, there's no way the element becomes detached between `findAllByRole()` and `.eq(2)`, so it must be one of the commands prior to this one. Could you show the full test? –  May 05 '21 at 09:38
  • 1
    BTW is it a React app? React re-renders frequently, it's a common cause of detached elements. –  May 05 '21 at 09:39
  • It is a react app, but how can finding an element with cypress cause a rerender? Nothing else is happening asynchronously at this point in my testing... – Gwynn May 06 '21 at 23:02
  • See here: https://github.com/cypress-io/cypress/issues/7306#issuecomment-997271455 –  Dec 18 '21 at 23:21

12 Answers12

33

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

cy.findAllByRole('rowheader').eq(2).click({force: true})
Ε Г И І И О
  • 11,199
  • 1
  • 48
  • 63
26

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()
      }
    }
Lane
  • 6,532
  • 5
  • 28
  • 27
Fody
  • 23,754
  • 3
  • 20
  • 37
13

Do NOT try forcing your click/action, instead use Cypress aliases and get the most of 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***);

Even though I'm traversing a lot 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.

Antony Fuentes
  • 1,013
  • 9
  • 13
12

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.

KyleMit
  • 30,350
  • 66
  • 462
  • 664
Chris Coulon
  • 121
  • 2
  • 1
    Do you know if your component has multiple render cycles? The only thing I have thought of, is maybe on the first render cycle the get() and should('be.visible') are validated, and in that timeframe the second render cycle begins and so the element gets replaced by the time the .click() runs and so it fails. That would explain why the static wait would fix the issue. – Gwynn Jun 25 '21 at 18:43
  • 1
    Chris, could you add the code you ussed to add a static wait? – KyleMit Oct 26 '21 at 17:20
11

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();
Alapan Das
  • 17,144
  • 3
  • 29
  • 52
  • 8
    How can there be a race condition between two Cypress commands? They run sequentially. –  May 05 '21 at 03:14
  • How does this command avoid a race in a way that the question's `cy.findAllByRole('rowheader').eq(2).click();` doesn't? Wouldn't there be a possibly of detachment before the `.click()` in either case? What is magic about `should()` that eliminates that chance? – Grant Birchmeier Oct 27 '21 at 22:26
  • 3
    This is a good question, I will try to answer this. `should('be.visble')` or any should assertion will retry till the condition is met. IMO cypress detached error is a time game, assertions can buy us more time for DOM to get stabled so that the next cypress commands work successfully. I have seen multiple assertions work fine as well like `cy.get(’selector’.)should('be.visble’).and(‘have.text’, ’some text’)` for my work. You can also add timeouts as well by passing the options. Anything apart from using `cy.wait` is a good way. – Alapan Das Oct 28 '21 at 07:18
  • 1
    But as you mentioned that doesn’t guarantee that we won’t get the DOM detached error. I guess its more about trying different things and seeing what works. For investigation I refer this blog post https://glebbahmutov.com/blog/detached/. I have written this answer around 5 months back and a lot of things have changed. Cypress has introduced `Test retries` which can be a good way to deal with this. Also there is plugins like https://www.npmjs.com/package/cypress-wait-until also works well. Plus a lot of other people has written about what worked in their case, we can look into that also. – Alapan Das Oct 28 '21 at 07:21
  • 4
    Guards do not work either, eg.: `cy.get('button').should('be.visible').click()` fails at the `.click()` call. Cypress confirms the button is visible but it fails clicking at it. Now tell me that's not a bug in the product. – papaiatis Dec 01 '21 at 17:48
5

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();
KyleMit
  • 30,350
  • 66
  • 462
  • 664
Misha Gariachiy
  • 103
  • 2
  • 7
1

Adding this retry option as a custom command has been my golden ticket, and I've reused it heavily for items like tooltips or collapsible sections in Bootstrap Vue where hidden items time out before being rendered to the DOM.

https://github.com/cypress-io/cypress/issues/7306#issuecomment-851344276

  • 2
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31870223) – spaleet Jun 01 '22 at 15:50
0

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).

Alexander Santos
  • 1,458
  • 11
  • 22
0

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?

  • 1
    This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](https://stackoverflow.com/questions/ask). To get notified when this question gets new answers, you can [follow this question](https://meta.stackexchange.com/q/345661). Once you have enough [reputation](https://stackoverflow.com/help/whats-reputation), you can also [add a bounty](https://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question. - [From Review](/review/late-answers/30835650) – Luca Kiebel Jan 20 '22 at 11:51
0

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.

0

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.

Valentin
  • 1,371
  • 1
  • 11
  • 15
0

I've been in the same situation and found out 2 things that worked for me:

  1. Use {force:true}, but this is not the best decision
  2. Try starting with: cy.document().its('body')...
BaliGo
  • 1
  • 1