cypress icon indicating copy to clipboard operation
cypress copied to clipboard

Re-query elements that are found 'detached' from the DOM

Open jennifer-shehane opened this issue 4 years ago • 276 comments

Current status

This is currently in progress. See https://github.com/cypress-io/cypress/issues/7306#issuecomment-1182059194, https://github.com/cypress-io/cypress/issues/7306#issuecomment-1182427284 and https://github.com/cypress-io/cypress/issues/7306#issuecomment-1188245931 for a brief description of the plan and updates on current status.

Current behavior:

Currently when Cypress queries an element that was passed from a parent element (say in .get(), .click() or .find()), if the element at some point becomes detached from the DOM, we throw an error:

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

<element>

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

The previous command that ran was:

  cy.get()

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
Screen Shot 2020-11-02 at 10 27 54 AM

Desired behavior:

Really users just want to re-query the DOM for the previously found element and click on the new element. Because it's more often than not that the framework underneath has created a completely new DOM element and inserted it into the DOM.

The proposal is to re-query the previously found element and continue the command with any newfound elements. Perhaps this could be an option passed to the command to allow this or not.

Reproducible Example

Provided in https://github.com/cypress-io/cypress/issues/9032

index.html

<form>
    <select onchange="document.forms[0].submit()">
        <option value="1">First</option>
    </select>
    <input />
</form>

spec.js

it('this will fail but should not', () => {
    cy.visit('index.html')
    cy.get("select").select("First")
    // adding any wait time will make it pass
    // cy.wait(0)
    cy.get("input").type("Hallo")
})

Related issues

  • https://github.com/cypress-io/cypress/issues/6215
  • https://github.com/cypress-io/cypress/issues/5743
  • https://github.com/cypress-io/cypress/issues/4946
  • https://github.com/cypress-io/cypress/issues/4414
  • https://github.com/cypress-io/cypress/issues/3670
  • https://github.com/cypress-io/cypress/issues/9032

jennifer-shehane avatar May 12 '20 08:05 jennifer-shehane

Is there a workaround available in the meantime? Perhaps writing a custom uncaught:exception handler?

mf-alex-tsepkov avatar May 18 '20 13:05 mf-alex-tsepkov

Tracking this issue as it's affecting at least 20 of my automated scripts that contain an each loop to fail.

ghost avatar May 20 '20 16:05 ghost

Yeah, I've experienced this a lot too, yet to find a decent solution...really love cypress but this issue annoys me!

jhanzeb1 avatar May 21 '20 16:05 jhanzeb1

Per https://github.com/cypress-io/cypress/issues/5743#issuecomment-622596999:

.click({force: true})

... is a decent enough solution, but it's vague without a comment.

stevenvachon avatar May 21 '20 20:05 stevenvachon

@stevenvachon I tried that several times in several of the scripts and it still hasn't worked for me.

ghost avatar May 21 '20 21:05 ghost

We ran into this issue so many times it was making our tests very flakey. We've started using the waitUntil command this week and has made our tests much more reliable, I'd recommend giving it a go. https://github.com/NoriSte/cypress-wait-until#readme

FayP avatar May 22 '20 00:05 FayP

Per #5743 (comment):

.click({force: true})

... is a decent enough solution, but it's vague without a comment.

force may work but is not ideal here, since you're forcing your test to interact with an element the user may ultimately not be able to interact with (what if the element never gets reattached?). You want to wait until the element can be interacted with, not force interaction on a detached element. I currently have a cy.wait(5000) in our test, but that hack goes against the best practices of Cypress.

mf-alex-tsepkov avatar May 22 '20 02:05 mf-alex-tsepkov

@FayP how can you use waitUntil with an each loop? here is one of my each loops that I like to loop through.

cy.get(".mx-auto > .checkbox-col > .form-check")
          .each(($el, index, $list) => {
              cy.wrap($el).click({force: true});
              cy.get(".col-md-4 > .actions-contianer > .btn-secondary").click();
              cy.get(".mb-0 > :nth-child(2) > .btn").click();
              cy.get(".jobs-list-nav > :nth-child(1) > .nav-link").click();
          })

ghost avatar May 22 '20 02:05 ghost

@eabusharkh0 I'm not sure which element you're trying to target in that code snippet. But in theory you could target anything with a waitUntil. For example cy.waitUntil(() => cy.wrap($el))).click(); I would expect to work. Best bet is to give it a try.

FayP avatar May 22 '20 02:05 FayP

So I managed to get it working (at least in few of my tests) with wait-until plugin:

cy.waitUntil(() =>
      cy.get('.someSelector')
        .as('someAlias')
        .wait(10) // for some reason this is needed, otherwise next line returns `true` even if click() fails due to detached element in the next step
        .then($el => Cypress.dom.isAttached($el)),
    { timeout: 1000, interval: 10 })

      .get('@someAlias')
      .click()

TomaszG avatar May 29 '20 14:05 TomaszG

@TomaszG A plugin, a wait, an additional check in a loop, an additional selector. All I can say is.... yikes. Cypress is kinda shitting the bed here.

JonDum avatar Jun 01 '20 22:06 JonDum

So I managed to get it working (at least in few of my tests) with wait-until plugin:

cy.waitUntil(() =>
      cy.get('.someSelector')
        .as('someAlias')
        .wait(1) // for some reason this is needed, otherwise next line returns `true` even if click() fails due to detached element in the next step
        .then($el => Cypress.dom.isAttached($el)),
    { timeout: 1000, interval: 10 })

      .get('@someAlias')
      .click()

Unfortunately, it does not work for me. It works fine for the cy.get(...) chain but fails for the cy.get(...).find(...) chain because Cypress cannot find the parent element.

I solved my issue using the JQuery.click():

cy
  .get('[data-cy="user_row"]')
  .find('[data-cy="user_row_cell"]')
  .should('be.visible')
  .then((e) => {
    Cypress.$(e).click();
  })

It does not work for all cases but it works fine for my case. Maybe it will be useful to someone.

serge-shaldanov avatar Jun 05 '20 21:06 serge-shaldanov

All solutions here are at best hacky workarounds. The Cypress spirit is to interact with UIs like an user would do. This issue is indeed very technical and obviously an user is not supposed to know if an element is render one or multiple times, so as the test writer.

IMO, This re-query should be done systematically, without option to enforce the Retry-ability core-concept of Cypress.

For now, my only not-100%-safe workaround is a cy.wait(xxx) before any risky selector. It goes against core-concepts of this project.

pmaoui avatar Jun 06 '20 16:06 pmaoui

AngularJS and Angular users can use https://angular.io/api/core/Testability

We have something like this in our legacy AngularJS tests. I don't know if React has an equivilent api.

Cypress.Commands.add('waitUntilAngularStable', () => {
    const getAngular = () => cy.window().its('angular', {log: false});
    getAngular().then((angular) => {
        return new Cypress.Promise(resolve => {
            angular.getTestability('body').whenStable(() => {
                resolve();
            });
        });
    });
});

Used like

cy.get('.back-btn').click();
cy.waitUntilAngularStable();
cy.get('.container').should('not.be.visible');

marmite22 avatar Jun 10 '20 12:06 marmite22

Nothing works for me. Even chains fail. Снимок экрана 2020-06-10 в 16 46 29

One viable alternative is to use Cypress.$ instead of cy.get. It's something we've used to great affect in our team. The side effect is that it bypasses a lot of the checks that come with using the official selection method.

Copy-paste the following to your ./cypress/support/commands.js file.

Cypress.Commands.add("get$", (selector) => {
  return cy.wrap(Cypress.$(selector)).should("have.length.gte", 1);
});

And now you should be able to click the button using the cy.get$ command:

cy.get$("#btnId").click();

This works because cy.wrap automatically retries when the .should assertion passes. If you wanted to explicity wait until the DOM element is attached, you can try the solution here: https://github.com/cypress-io/cypress/issues/5743#issuecomment-650421731

Adil-Iqbal avatar Jun 11 '20 20:06 Adil-Iqbal

All solutions here are at best hacky workarounds. The Cypress spirit is to interact with UIs like an user would do. This issue is indeed very technical and obviously an user is not supposed to know if an element is render one or multiple times, so as the test writer.

IMO, This re-query should be done systematically, without option to enforce the Retry-ability core-concept of Cypress.

For now, my only not-100%-safe workaround is a cy.wait(xxx) before any risky selector. It goes against core-concepts of this project.

The time of a machine's interactions with elements on a site differ significantly than of a regular user's interactions. By a lot, in fact! That is why such things happen.

I am currently having the same issue with a <ul> list element. This particular list element updates its contents (<li> children) by the time a search box (text input element) receives input and basically acts as a search result suggestions list for the user.

As the user is typing into the searchbox, the suggestions refresh rather quickly on the server, probably on every character input, but does not manage to update in real-time for the user as well, which is why it takes a bit of time for the suggestions to refresh after the user is done typing. A user would typically wait a bit before the different suggestions calm down and stay in place, then click on the right one to advance.

This is why cy.wait(n); should not be ignored! Let's put the machine (test) into action now.

As the machine is done typing, the suggestions list might not have updated for the cilent (machine) instantly, however, the particular suggestion that meets the criteria is found at the bottom of the list, but would appear at the top right after the suggestions list finally updates for the client. The machine should now wait a bit until it is assumed that the list has been updated for the client and ready for its elements to be interacted with. Otherwise, the machine would select the suggestion found at the bottom and by the time the suggestion has been "moved" to the top, the machine will try and interact with it (or click on it), but the element is not there anymore. It has been moved, but in reality, its current instance has been removed from the DOM and a new instance has appeared on the top of the list as the list has been fully updated for the client.

Sorry for my bad english, but I'm hoping I covered some benefits of cy.wait(n); :) It is still not-100% safe, but is the safest approach if your assumptions are relatively correct.

jupiterspot avatar Jun 16 '20 12:06 jupiterspot

@Uberwire that is not a valid use of Cy.wait. Cy.get has a built in wait so if your css selector is correct it will wait till it becomes the first element in the list.

remjx avatar Jun 16 '20 16:06 remjx

I was not sure that my Cy ran synchronously, so I deleted my previous comment and rebuilt my tests. I separated actions and asserts to different describe/it sections and now it must work synchronously. But I get the error from time to time. Also I tried force click, it adds more problems, than solves.

azgnetov avatar Jun 16 '20 17:06 azgnetov

@Uberwire that is not a valid use of Cy.wait. Cy.get has a built in wait so if your css selector is correct it will wait till it becomes the first element in the list.

According to my context, cy.get gets called once on the object before it gets detached from the DOM, which terminates its "waiting". So, the point is to prevent getting the object until we assure that it will stay in place for an actual user to interact with it.

jupiterspot avatar Jun 16 '20 18:06 jupiterspot

@Uberwire in your example, you should be able to wait for the element and position within the container since it should be deterministic that once you've entered X characters should it appear at the top of the list.

remjx avatar Jun 16 '20 22:06 remjx

@Uberwire in your example, you should be able to wait for the element and position within the container since it should be deterministic that once you've entered X characters should it appear at the top of the list.

Safer and more practical approach in that case. Unless, the element an actual user is looking for gets found at the bottom of the list while another element that meets the same criteria gets found at the top of the list before the list fully refreshes, getting caught in the get method before the actual element we want.

The user can stop typing mid-way after seeing the desired result at the bottom of the list and interact, but that is not what is being tested in my case.

jupiterspot avatar Jun 17 '20 12:06 jupiterspot

cy.contains('elementToRequery') seems to work for us, since we rely a lot on loading components asynchronously. 🥳 this doesn't work however in combination with cy.get() (as in cy.get.contains('elementToRequery')

more on this behaviour here

kkukelka avatar Jun 20 '20 06:06 kkukelka

I have the same problem and I have been days without progress, any solution?

 cy.get('[data-cy=acceptButton]').click()

it's not clicking

berazur avatar Jun 22 '20 15:06 berazur

Same problem even if using the waitUntil library:

cy.get('[data-cy=acceptButton]').click({ force: true })
cy.waitUntil(  () => cy.get('[data-cy=acceptButton]')  ).click()
cy.waitUntil(  () => cy.get('[data-cy=acceptButton]')  ).click({ force: true })

element is DETACHED from the DOM (not always, but randomly)

mikepetrusenko avatar Jun 23 '20 07:06 mikepetrusenko

How can we trust the cypress test if it doesn't work as expected?

cy.get('[data-cy=acceptOfferButton]').as('acceptOfferButton')
cy.get('@acceptOfferButton').should('be.visible')
cy.get('@acceptOfferButton').click({ force: true })

all commands pass successfully except last, sometimes is clicking but most of the time not.

How can we work with react component if the framework re-rendendered asynchronously?

berazur avatar Jun 23 '20 08:06 berazur

Thanks @marmite22 for your solution. It helped me to resolve the "CypressError: Timed out retrying: cy.click() failed because this element is detached from the DOM." error message for Angular2+.

Here is the solution I came with :

export const waitUntilAngularStable = () =>
  cy
    .window()
    .invoke("getAllAngularRootElements")
    .then(ngRootElements => {
      cy.window()
        .invoke("getAngularTestability", ngRootElements[0])
        .then(
          testability =>
            new Cypress.Promise(resolve => {
              testability.whenStable(() => {
                resolve();
              });
            })
        );
    });

Cypress.Commands.add("waitUntilAngularStable", waitUntilAngularStable);

Inside your test : cy.waitUntilAngularStable();

cm0s avatar Jun 25 '20 16:06 cm0s

@cm0s This is an interesting idea. How do you know that the click will take place when waitUntilStable is still true? Is it possible to chain the check immediately before the click?

bcole-dbg avatar Jun 25 '20 16:06 bcole-dbg

@bcole-dbg, what currently do is juste that :

    // before waitUntilAngularStable, do stuff that my rerender some angular elements and make it "unstable"
    cy.waitUntilAngularStable();
    // DOM elements have finished rerendering we can trigger our click event
    cy.get(".my-button").click();

In my understanding, the click event will not be triggered before the promise in the waitUntilAngularStable() is resolved, so we should be good. But perhaps there is something I don't understand, on how Angular cycles work with Cypress. For now, it seems to work with my use case. I will be happy to hear if others try this and can improve it.

cm0s avatar Jun 25 '20 19:06 cm0s

Used command solution posted by wintonpc with success! https://github.com/cypress-io/cypress/issues/5743#issuecomment-650421731

fjolin-intuit avatar Jun 30 '20 00:06 fjolin-intuit

Any word on this? I was using a test spec and my new way of selecting users in a table worked great, but once i put it to use in my spec proper things started to detach. My guess is it's moving from one IT to the next and when it hits the new IT the item detaches.

This is really annoying, when a test becomes flakey when using cy.contains('td', 'Automated User').siblings().eq(0).children().eq(0).click({force: true}); as the test gets detached by the second eq. If i use force:true the click may or may not actually check the checkbox, even though it "passes". It's clearly not clicking the checkbox.

DRiewaldt avatar Jul 15 '20 23:07 DRiewaldt