Error message for cypress - the element has become detached or removed from the dom

Posted by enkidu72 on Tue, 11 Jan 2022 05:54:55 +0100

For the analysis and solution of this error message, please refer to the official website of Cypress file.

This error message reminds us that the Cypress code we write is interacting with a "dead" DOM element.

Obviously, in a real usage scenario, a user cannot interact with this type of UI element.

Take a practical example:

<body>
  <div id="parent">
    <button>delete</button>
  </div>
</body>

After this button is clicked, it will remove itself from the DOM tree:

$('button').click(function () {
  // when the <button> is clicked
  // we remove the button from the DOM
  $(this).remove()
})

The following line of test code causes an error:

cy.get('button').click().parent()

When cypress executes the next parent command, it is detected that the button applying the command has been removed from the DOM tree, so an error will be reported.

Solution:

cy.get('button').click()
cy.get('#parent')

Guidelines for solving such problems:

Guard Cypress from running commands until a specific condition is met

There are two ways to implement guard:

  1. Writing an assertion
  2. Waiting on an XHR

Look at another example:

Enter clem and select User clementine from the result list, which is the so-called type head search effect.

The test code is as follows:

it('selects a value by typing and selecting', () => {
  // spy on the search XHR
  cy.server()
  cy.route('https://jsonplaceholder.cypress.io/users?term=clem&_type=query&q=clem').as('user_search')

  // first open the container, which makes the initial ajax call
  cy.get('#select2-user-container').click()

  // then type into the input element to trigger search, and wait for results
  cy.get('input[aria-controls="select2-user-results"]').type('clem{enter}')

  // select a value, again by retrying command
  // https://on.cypress.io/retry-ability
  cy.contains('.select2-results__option', 'Clementine Bauch').should('be.visible').click()
  // confirm Select2 widget renders the name
  cy.get('#select2-user-container').should('have.text', 'Clementine Bauch')
})

main points:

Use cy.route to monitor an XHR. Can relative paths be monitored here?

If the local test passes, the following errors will be encountered when running on the CI:

How to analyze this problem? You can use the pause operation to pause the test runner.

// first open the container, which makes the initial ajax call
cy.get('#select2-user-container').click().pause()

When we click the select2 widget, an AJAX call will be triggered immediately The test code does not wait for the return of the clem search request. It just looks for the DOM element of "Clementine Bauch".

// first open the container, which makes the initial ajax call
cy.get('#select2-user-container').click()

// then type into the input element to trigger search,
// and wait for results
cy.get('input[aria-controls="select2-user-results"]').type('clem{enter}')

cy.contains('.select2-results__option',
   'Clementine Bauch').should('be.visible').click()

The above test may pass most of the time when running locally, but it may often fail on CI because network calls are slow and browser DOM updates may be slow. Here is how tests and applications enter the race conditions that cause "detached element" errors.

  1. Test click widget
  2. The Select2 widget triggers the first search Ajax call. On a CI, this call may be slower than expected.
  3. The test code enters "clem" to search, which triggers a second AJAX call.
  4. The Select2 widget receives a response to the first search Ajax call with ten user names, one of which is "Clementine Bauch". These names are added to the DOM

Then test the search for the visible selection "Clementine Bauch" - and find it in the initial user list.

Then, the test runner will click on the element to find. Notice the state condition here. When the second search Ajax call "term=clem" returns from the server. The Select2 widget deletes the current list of options and displays only two found users: "Clementine Bauch" and "Clementina DuBuque".

Then the test code executes the click of the Clem element.

Cypress throws an error because the DOM element with the text "Clementine Bauch" it wants to click is no longer linked to the HTML document; It has been removed from the document by the application, and cypress still references the element.

This is the root of the problem.

The following code can artificially make this state condition always trigger:

cy.contains('.select2-results__option',
            'Clementine Bauch').should('be.visible')
  .pause()
  .then(($clem) => {
    // remove the element from the DOM using jQuery method
    $clem.remove()
    // pass the element to the click
    cy.wrap($clem)
  })
  .click()

Now that we understand the root cause of the state condition trigger, there is a direction for correction.

We want the test to always wait for the application to complete its operation before continuing.

Solution:

cy.get('#select2-user-container').click()

// flake solution: wait for the widget to load the initial set of users
cy.get('.select2-results__option').should('have.length.gt', 3)

// then type into the input element to trigger search
// also avoid typing "enter" as it "locks" the selection
cy.get('input[aria-controls="select2-user-results"]').type('clem')

We passed cy.get('XXX ') The should(') operation ensures that the AJAX corresponding to the initial user list must be returned to the server before entering the clem. Otherwise, the length of select2 options must be less than 3

When the test types "clem" in the search box, the application triggers an Ajax call that returns a subset of the user. Therefore, the test needs to wait for the new set to display - otherwise it will find "Clementine Bauch" from the initial list and encounter a detached error. We know that only two users match "clem", so we can reconfirm the number of users displayed and wait for the application.

/ then type into the input element to trigger search, and wait for results
cy.get('input[aria-controls="select2-user-results"]').type('clem')

// flake solution: wait for the search for "clem" to finish
cy.get('.select2-results__option').should('have.length', 2)

cy.contains('.select2-results__option', 'Clementine Bauch')
    .should('be.visible').click()

// confirm Select2 widget renders the name
cy.get('#select2-user-container')
  .should('have.text', 'Clementine Bauch')

If you blindly pass in the force:true parameter in the click call, new problems may be introduced.