Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

After upgrade to version 3.6.1 tests start failing with " CypressError: Timed out retrying: cy.click() failed because this element is detached from the DOM" #5743

Closed
mschaaf opened this issue Nov 19, 2019 · 27 comments
Labels
type: duplicate This issue or pull request already exists

Comments

@mschaaf
Copy link

mschaaf commented Nov 19, 2019

Current behavior:

After upgrade to 3.6.1 from 3.4.1 our cypress tests start to fail for the following constructs.

cy.get('fieldset > legend > i').should('be.visible').click()

fails with

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

<i expander="" id-to-expand="login-username-password-content" expanded="false" class="fa fa-plus-square ng-isolate-scope"></i>

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

The previous command that ran was:

  > cy.should()

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

Common situations why this happens:
  - Your JS framework re-rendered asynchronously
  - Your app code reacted to an event firing and removed the element

You typically need to re-query for the element or add 'guards' which delay Cypress from running new commands.

https://on.cypress.io/element-has-detached-from-dom
      at Object.cypressErr (/__cypress/runner/cypress_runner.js:104968:11)
      at Object.throwErr (/__cypress/runner/cypress_runner.js:104923:18)
      at Object.throwErrByPath (/__cypress/runner/cypress_runner.js:104955:17)
      at Object.retry (/__cypress/runner/cypress_runner.js:96290:16)
      at retryActionability (/__cypress/runner/cypress_runner.js:85007:19)
      at tryCatcher (/__cypress/runner/cypress_runner.js:139045:23)
      at Function.Promise.attempt.Promise.try (/__cypress/runner/cypress_runner.js:136320:29)
      at tryFn (/__cypress/runner/cypress_runner.js:96748:21)
      at whenStable (/__cypress/runner/cypress_runner.js:96783:12)
      at /__cypress/runner/cypress_runner.js:96333:16
      at tryCatcher (/__cypress/runner/cypress_runner.js:139045:23)
      at Promise._settlePromiseFromHandler (/__cypress/runner/cypress_runner.js:136981:31)
      at Promise._settlePromise (/__cypress/runner/cypress_runner.js:137038:18)
      at Promise._settlePromise0 (/__cypress/runner/cypress_runner.js:137083:10)
      at Promise._settlePromises (/__cypress/runner/cypress_runner.js:137162:18)
      at Promise._fulfill (/__cypress/runner/cypress_runner.js:137107:18)

We followed the link from the error message and tried to change code to:

cy.get('fieldset > legend > i').should('be.visible');
cy.get('fieldset > legend > i').click()

But this didn't change the behaviour. Also the element should be visibe from the beginning so not code is changing it in an async way.

Desired behavior:

Should not fail.

Steps to reproduce: (app code and test code)

For now we don't have them because not everytime the same tests fail. The only common thing between them is the same or similar click behavior.

These constructs were not failing with version 3.4.1.

Versions

Ubuntu 19.04
cypress 3.6.1
Electron 73

We run the tests via yarn cypress run

Any help to find the reason would be appreciated.

@mschaaf mschaaf changed the title After upgrade to version 3.6.1 tests start failig After upgrade to version 3.6.1 tests start failing with " CypressError: Timed out retrying: cy.click() failed because this element is detached from the DOM" Nov 19, 2019
@mschaaf mschaaf closed this as completed Nov 19, 2019
@mschaaf mschaaf reopened this Nov 19, 2019
@jennifer-shehane
Copy link
Member

Could you update to the current version of Cypress and let me know if this is still happening for you? Your issue may have already been fixed as I know we addressed an issue similar to this.

@cypress-bot cypress-bot bot added the stage: awaiting response Potential fix was proposed; awaiting response label Jan 2, 2020
@chopraapooja
Copy link

chopraapooja commented Jan 3, 2020

Faced the same problem with latest version 3.8.1 on React SPA,

cy.get('.some-css-class').then(($el) => {
      console.log(Cypress.dom.isDetached($el)) // false
}).click();

Initially cy.get() is able to find the element, in-between React kicks rendering cycle and click() is not able to see the reference of the element. Any clue ?

@smart625
Copy link

smart625 commented Jan 16, 2020

@jennifer-shehane Faced the same issue with the latest version 3.8.1. Can you take a look at it. It blocked me for a while.

cy.get('#wact_rct_39').trigger('mouseover','bottom')
          .trigger('mousedown','bottom', {force: true}).wait(300)
          .trigger('mousemove', { clientX: 200, clientY: 630 })
         .trigger('mouseup', {force: true})

After trigger mousedown, there is a XHR request and then when execute mousemove, then error show on page.
image

@roblevintennis
Copy link

Not sure if it's helpful at all, but I found the error docs for this error quite enlightening and helpful. I literally fixed it by heeding the recommendations.

I'm sure your selectors will be different, but here's the diff that fixed my same issue:

image

When I read the docs it talked about how frameworks will mount/unmount behind the scenes in a way that's not visible to our naked eyes, and that really hit home. We too are using React. But probably would be same issue for Angular, Ember, etc., maybe.

Finding a way to simplify and break apart my query's made the tests pass for this one.

Hope it's somehow helpful even if pretty high level and hand wavy :)

@jennifer-shehane jennifer-shehane removed the stage: awaiting response Potential fix was proposed; awaiting response label Feb 4, 2020
@jennifer-shehane
Copy link
Member

Yes, please read the recommendations from the error message for this error here: https://on.cypress.io/error-messages#cy-failed-because-the-element-you-are-chaining-off-of-has-become-detached-or-removed-from-the-dom

There's not much Cypress can do with an element that no longer exists in the DOM, so you will need to restructure your tests to account for this is your framework mounts/unmounts DOM elements.

@mschaaf
Copy link
Author

mschaaf commented Feb 4, 2020

@jennifer-shehane Please read the description of the issue. I wrote that we followed the advice and it didn't solve the issue. So if you close the issue I expect from you that you can show me where we didn't follow this advice. Thank you.

The comment from @roblevintennis seems unrelated and closing the issue because of it is wrong IMO.

@giladv
Copy link

giladv commented Mar 24, 2020

having the same issue

@leogoesger
Copy link

same issue, the element does exist on the dom, but cannot find it. It works well in local, but fails in CI.

@bxt
Copy link

bxt commented Apr 6, 2020

@jennifer-shehane I think it's really sad that this was closed. Cypress docs mention in so many places how important it is to have tests not be flaky, and in this case a very simple cy.get('button').click() gets very flaky just because you use e.g. React.

Frameworks like React might chose to re-render buttons for various reasons, and you even acknowledged this in your error message "Common situations why this happens: - Your JS framework re-rendered asynchronously" but then you fail to provide a solution for this situation, let me explain:

The reasons for a re-render can be many, and e.g. occur in a completely different feature on the same page, while it is still loading or running animations. And in your conditional testing guide you write yourself:

To do this would require you to know with 100% guarantee that your application has finished all asynchronous rendering and that there are no pending network requests, setTimeouts, intervals, postMessage, or async/await code.

This is difficult to do (if not impossible) without making changes to your application. You could use a library like Zone.js, but even that does not capture every async possibility.

So I think your proposed solutions "Re-query for newly added DOM elements" or "Guard Cypress from running commands until a specific condition is met" will fall short for a reasonably complex application. In fact I ran into this problem on a big SPA with dozens of developers working on it, and some component from another team finishes loading in the middle of our test.

To solve the problem I think Cypress should just re-try and look for a new button that matches the selector. Or there should be at least a simple example code snippet on how to solve the generic problem of having a framework re-render between selector and click.

@scottybollinger
Copy link

Following because we are having the same issue. Hoping this gets reopened and resolved. As with @leogoesger, our tests pass every time locally but fail on CI intermittently

@astapenkoav
Copy link

The same problem with SPA on AngulaJS.

@vmtl-adsk
Copy link

Faced this issue on 3.8.0 with React

@egretsRegrets
Copy link

egretsRegrets commented May 1, 2020

If anyone is encountering this issue with .click(), I found that .click({force: true}) circumvented the problem in my case.

I'm using Angular 9 and encountered the issue when trying to access a menu item of a Mat Menu (angular material) component.

The menu rendering is all js, so I would not be surprised if the issue resulted from some tricky re-rendering behavior. I first tried to simplify the command (removing commands between the .get() and .click(), as suggested in the simplified example in the docs) but that didn't seem to help, in the end I was still seeing the issue with just cy.get(.my-class-name).click(); as with other posters, the test runner showed get() logging the expected element, but click() failed with the detached element issue.

@vmtl-adsk
Copy link

Faced this issue on 3.8.0 with React

Found some possible solution / workaround, which helped in my case: I had an element, which after some events would get an id attribute (I didn't notice that) and cypress somehow was trying to interact with the element without that id attribute (contains, should etc would fail) - thanks to cypress ui. The only thing helped was to use invoke and then do other actions after a check on the id

@taylorjdawson
Copy link

I think @egretsRegrets suggestion of using { force: true } is the key here. To Cypress, the button may appear to be detached from the DOM (because of re-rendering) but from the user perspective they still have the ability to click the button (at least in my particular case).
When using { force: true } Cypress will skip the check that ensures it is attached to the DOM. See docs.

@Aeolun
Copy link

Aeolun commented May 11, 2020

Just for the record. I think it's absolutely insane that the last version of Cypress that actually works for us is 3.4. How is it possible that this flow is completely broken on newer version of Cypress?

Why doesn't Cypress just re-query the element if it finds it detached? That's literally what it does for every other form of 'get' and apparently did previously.

@jennifer-shehane
Copy link
Member

@Aeolun We're open to PRs to implement this retry mechanism. That would be a pretty large change to our current implementation. I've opened an issue requesting this feature here: #7306

@wintonpc
Copy link

I had a problem similar to what @chopraapooja described. The following solved it for me.

/**
 * getAttached(selector)
 * getAttached(selectorFn)
 *
 * Waits until the selector finds an attached element, then yields it (wrapped).
 * selectorFn, if provided, is passed $(document). Don't use cy methods inside selectorFn.
 */
Cypress.Commands.add("getAttached", selector => {
  const getElement = typeof selector === "function" ? selector : $d => $d.find(selector);
  let $el = null;
  return cy.document().should($d => {
    $el = getElement(Cypress.$($d));
    expect(Cypress.dom.isDetached($el)).to.be.false;
  }).then(() => cy.wrap($el));
});

This works because cy.should retries, and the element is re-queried each time.

Example usage:

cy.getAttached("button").click();
cy.getAttached($d => $d.find("button")).click();

The second form lets you use arbitrary logic for finding the element.

@brandenbyers
Copy link

Thanks you @wintonpc, your solution nearly fixed the issue I have been experiencing. The only addition that I needed to make for an Ember app was to add an optional contains parameter to the getAttached method. And then I added a second expectation:

expect($el).to.contain(contains)

That way, I was able to call getAttached() and pass in expected text. This replaced my faulty cy.get().should().click() chain that would always fail on the should() for a DOM element that was occasionally detaching and reattaching after Cypress entered a search query and the DOM was updated, potentially multiple times, before clicking.

@mikepetrusenko
Copy link

@wintonpc Could we say this is 100% working solution? I've tried your approach and still getting the 'detach from the DOM' :(

@czo02
Copy link

czo02 commented Jul 5, 2020

@wintonpc Could we say this is 100% working solution? I've tried your approach and still getting the 'detach from the DOM' :(

Same here. We tried the getAttached solution but it didn't work for us. It seems that the element is detached between the check and the click which suggests that it happens very quickly.

The only workaround for us is a cy.wait(50).

@wintonpc
Copy link

wintonpc commented Jul 6, 2020

@mikepetrusenko I don't think there's a single solution that will work 100% of the time for all applications. What we're really trying to do is wait for the app to complete its DOM updates; waiting for a particular element to be attached is just one way to do that. For my particular app, it worked. For others, it might not. To have rock-solid tests, you're going to need a good understanding of how your app and the framework it uses (Vue.js, React, etc.) work together to update the DOM.

In @czo02 's case, getAttached failed because it couldn't tell the difference between "still attached" and "newly attached".

Here's another approach that might be more reliable. The following command waits for Vue to stop updating the DOM. If the specified number of milliseconds elapse without any new updates, it considers the updates to have completed.

Cypress command:

Cypress.Commands.add("waitForVue", millis => {
  millis = millis || 100;
  let startTime = null;
  return cy.window().should(w => {
    startTime = startTime || Date.now();
    expect(Date.now()).to.be.greaterThan(Math.max(startTime, w.lastVueUpdate || startTime) + millis);
  });
});

Vue hook (in the app under test):

updated() {
  if (window.Cypress) // we're being tested
    this.$nextTick(() => window.lastVueUpdate = Date.now());
}

Usage:

cy.get("button:contains(Show Test Button)"); // triggers DOM update
cy.waitForVue(100);
cy.get("button:contains(Test)").click();

I'm not familiar with frameworks other than Vue, but I assume they have similar ways to inform you when updates occur.

The advantage of waitForVue() over cy.wait() is that, because it accounts for updates, you can theoretically set a lower timeout for waitForVue() than you could for cy.wait() and have them be equally reliable.

@Izhaki
Copy link

Izhaki commented Jul 13, 2020

@wintonpc Solution didn't work for us, but was the basis for this:

/*
React elements may get detached between when we find them and when .click() is called.
Here we use JQuery .click() right after ensuring the element is attached.
Note that we cannot longer use the original cypress .click() API
Adaptation of https://github.com/cypress-io/cypress/issues/5743#issuecomment-650421731
*/

Cypress.Commands.add('clickAttached', { prevSubject: 'element' }, subject => {
  cy.wrap(subject).should($el => {
    expect(Cypress.dom.isDetached($el)).to.be.false; // Ensure the element is attached

    // Using Jquery .click() here so no queuing from cypress side and not chance for the element to detach
    $el.click();
  });
});

Sadly one cannot override the click command (Cypress.Commands.overwrite('click', ...)) because this breaks .type() (see issue).

JamesCullum added a commit to OWASP/SSO_Project that referenced this issue Aug 10, 2020
@ryan-mulrooney
Copy link

Agh. I'm experiencing this a lot with the React app I'm testing too. :(

TijlB99 added a commit to luciad/typescript-documentation that referenced this issue Aug 16, 2020
@pckhoi
Copy link

pckhoi commented Nov 3, 2020

I don't expect this to work for everyone but what improved flakiness for me is a simple assertion:

cy.get(".preview-btn")
  .should("be.visible")
  .click();

@laerteneto
Copy link

I am using cy.contains('some text') before clicking in the element I need to click. This is the only way I got to avoid this problem right before clicking in it.

So, it is basically like this:

cy.contains(item) // avoid element detached from the DOM
cy.get(element).click()

@jennifer-shehane
Copy link
Member

jennifer-shehane commented May 5, 2021

This issue will be closed to further comments. Please refer to #7306 for discussion on 'detached from the DOM' strategies and workarounds.

@cypress-io cypress-io locked as off-topic and limited conversation to collaborators May 5, 2021
@jennifer-shehane jennifer-shehane added the type: duplicate This issue or pull request already exists label May 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
type: duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests