Resource library

QA How-To

How to Fix Cypress "Element is detached from the DOM"

Fix Cypress element is detached from the DOM errors with fresh queries, network waits, runnable examples, CI checks, and guidance for unstable UI renders.

23 min read | 3,453 words

TL;DR

To fix Cypress Element Is Detached From The DOM, end the old command chain after a state-changing action and query the target again from the current document. Wait for a real ready state, then assert the user-visible result; repair continuous remounts in the application.

Key Takeaways

  • A detached subject is a specific DOM node that the application removed, even if a replacement looks identical.
  • Start a fresh Cypress query after actions or assertions that cross a rendering boundary.
  • Do not expect cy.wrap of a saved jQuery element to re-query the document.
  • Wait for the matching network request and assert the rendered ready state before acting.
  • Scope rebuilt list actions by stable record identity and assert the selected outcome.
  • Investigate continuous remounts as possible product defects, especially when users lose focus.
  • Verify the repaired user journey in CI with meaningful outcome assertions.

To fix Cypress Element Is Detached From The DOM, find the command that kept an element after the application replaced it. This error commonly appears after a click, form edit, network response, list refresh, or dialog transition. Start a new cy.get() chain after the transition, then assert the state you expect before the next action.

A replacement can have the same label and position as the removed element. Cypress still sees two different DOM nodes. The example below gives you a local page and focused specs so you can reproduce each cause, apply a fix, and check it with a command.

The subject is no longer attached to the DOM

Cypress also describes this family of failures as a command that "failed because the page updated." The exact prefix names the command that failed. See the Cypress error reference for the official example.

TL;DR

End a command chain after an action that may update the page. Query the target again from the document, then assert the new state and act on the current node:

cy.get('[data-cy=save]').click()
cy.get('[data-cy=save]').should('be.visible')
cy.get('[data-cy=status]').should('have.text', 'Saved')

Use the second cy.get() only if the save button should still exist. If saving removes it, query the confirmation instead. When the replacement follows an API request, register cy.intercept() before triggering the request, wait for its alias, and assert the rendered result. Avoid cy.wait(1000) and click({ force: true }) as repairs. Neither makes an old node current.

What the Error Actually Means

A Cypress subject is the value yielded to the next command. A query such as cy.get('button') can retry while finding a suitable element. An action such as .click() executes once and yields the element on which it acted. If the click makes your app remove that button, a later .parent(), .should(), or second .click() in the same chain can still receive the old node. Cypress cannot safely replay the action because clicking twice could submit twice or delete two records.

This differs from "element not found." A detached element existed when Cypress selected it. It also differs from a covered or disabled element: those remain in the document but fail other actionability checks. Cypress retries queries and assertions, while action commands have separate actionability checks. The boundary between a retryable query and a fixed post-action subject explains why a fresh query works.

Do not assume every React render detaches nodes. React can update text or attributes in place. A changing key, conditional branch, route, virtualized row, or skeleton swap can remove a node. Use the Command Log and the actual app state to learn which transition happened. The Cypress retry-ability guide covers the broader query model.

Root-Cause Decision Table

Symptom Root cause Fix
.click().parent() fails after a button changes the page The action yielded the old button End the chain and query the current DOM
.type().clear() fails in a controlled form Input handling replaced the field Start a new cy.get() before each action
cy.wrap($el).click() fails after refresh $el is a captured jQuery collection Replace the wrapped object with a fresh selector query
Result action fails after loading A response swapped a placeholder or list Wait on the matching request and assert the ready state
A row action targets the wrong item after sort The row was rebuilt or its position changed Re-query by stable record identity
A dialog button fails after reopening Closing unmounted the dialog subtree Query the newly opened dialog
.should(...).find(...) fails during render An assertion locked in a subject before a later query Restart the query chain from the document
Fresh queries keep failing The application repeatedly remounts the target Fix the render lifecycle or expose a stable ready state

The table is a triage aid, not a list of interchangeable waits. Check the first failing command, then trace back to the immediately preceding event that could have replaced its subject. A network wait helps only if that request is actually the transition that renders the target.

Reproducible Local Setup

Create a scratch directory outside your application repository. Run npm init -y and npm install --save-dev cypress, using the Cypress version compatible with your project. Do not copy a guessed version number into a lockfile. Save this page as site/detached-demo.html. It uses ordinary browser APIs and deliberately replaces several nodes so each fix has a concrete outcome.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Detached DOM demo</title>
<body>
  <button data-cy="swap">Replace on click</button>
  <p data-cy="swap-status"></p>
  <label>Name <input data-cy="name"></label>
  <button data-cy="submit" disabled onclick="document.querySelector('[data-cy=form-status]').textContent='Submitted'">Submit</button>
  <p data-cy="form-status"></p>
  <button data-cy="refresh">Refresh target</button>
  <button data-cy="cached" onclick="document.querySelector('[data-cy=cache-status]').textContent='Clicked current target'">Target</button>
  <p data-cy="cache-status"></p>
  <button data-cy="load">Load items</button>
  <p data-cy="load-status">Idle</p>
  <ul data-cy="items"></ul>
  <button data-cy="sort">Sort rows</button>
  <ul data-cy="rows"><li data-id="a">Alpha <button>Choose</button></li><li data-id="b">Beta <button>Choose</button></li></ul>
  <p data-cy="selection"></p>
  <button data-cy="open">Open dialog</button>
  <div data-cy="dialog" hidden></div>
  <p data-cy="dialog-status"></p>
  <button data-cy="remount">Remount target</button>
  <button data-cy="stable" onclick="document.querySelector('[data-cy=stable-status]').textContent='Stable action worked'">Stable action</button>
  <p data-cy="stable-status"></p>
<script>
  const one = selector => document.querySelector(selector)
  one('[data-cy=swap]').addEventListener('click', event => {
    event.currentTarget.replaceWith(event.currentTarget.cloneNode(true))
    one('[data-cy=swap-status]').textContent = 'Replacement is present'
  })
  one('[data-cy=name]').addEventListener('input', event => {
    const old = one('[data-cy=submit]')
    const next = old.cloneNode(true)
    next.disabled = !event.target.value.trim()
    old.replaceWith(next)
  })
  one('[data-cy=refresh]').addEventListener('click', () => {
    const old = one('[data-cy=cached]')
    old.replaceWith(old.cloneNode(true))
  })
  one('[data-cy=load]').addEventListener('click', async () => {
    one('[data-cy=load-status]').textContent = 'Loading'
    const response = await fetch('/api/items')
    const items = await response.json()
    one('[data-cy=items]').innerHTML = items.map(item => `<li>${item}</li>`).join('')
    one('[data-cy=load-status]').textContent = 'Ready'
  })
  one('[data-cy=sort]').addEventListener('click', () => {
    one('[data-cy=rows]').innerHTML = '<li data-id="b">Beta <button>Choose</button></li><li data-id="a">Alpha <button>Choose</button></li>'
  })
  one('[data-cy=rows]').addEventListener('click', event => {
    if (event.target.tagName === 'BUTTON') {
      one('[data-cy=selection]').textContent = event.target.closest('li').dataset.id
    }
  })
  one('[data-cy=open]').addEventListener('click', () => {
    const dialog = one('[data-cy=dialog]')
    dialog.hidden = false
    dialog.innerHTML = '<button data-cy="close">Close</button><button data-cy="apply">Apply</button>'
  })
  one('[data-cy=dialog]').addEventListener('click', event => {
    if (event.target.matches('[data-cy=close]')) {
      event.currentTarget.replaceChildren()
      event.currentTarget.hidden = true
    }
    if (event.target.matches('[data-cy=apply]')) {
      one('[data-cy=dialog-status]').textContent = 'Applied in current dialog'
    }
  })
  one('[data-cy=remount]').addEventListener('click', () => {
    const old = one('[data-cy=stable]')
    old.replaceWith(old.cloneNode(true))
  })
</script>
</body>
</html>

Save cypress.config.js with a local base URL. Serve the site directory in one terminal and run Cypress in another. Python's simple server returns a 404 for /api/items; the network test below intercepts that route before the browser requests it.

const { defineConfig } = require('cypress')
module.exports = defineConfig({
  e2e: { baseUrl: 'http://127.0.0.1:4173' },
})
python3 -m http.server 4173 --directory site
npx cypress run --spec cypress/e2e/detached.cy.js

The first command stays running. Create cypress/e2e/detached.cy.js from the tests in the sections that follow. Append each it(...) block directly to the named spec. Cypress accepts top-level tests, and each test visits the demo independently, so it does not rely on a prior test's DOM.

1. Fix Cypress Element Is Detached From The DOM After a Click

The shortest reproduction is a button that replaces itself in its click handler. A chain such as cy.get('[data-cy=swap]').click().parent() asks .parent() to start from the old button. The button's clone looks the same, but the original is no longer connected. Cypress's common error example uses this exact kind of replacement.

Use a new query for the post-click state. In the demo, the useful outcome is the status text. A second button query is included to prove that a current replacement exists. The assertion does not claim that the original node survived.

it('queries again after a click replaces its subject', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=swap]').click()
  cy.get('[data-cy=swap-status]').should('have.text', 'Replacement is present')
  cy.get('[data-cy=swap]').should('be.visible')
})

Verify with npx cypress run --spec cypress/e2e/detached.cy.js. You should see this test pass. In your own suite, substitute the actual confirmation, route, or row state that a user should see after the click. If the action deletes a row, assert that the row no longer exists instead of querying a replacement button.

2. Fix Cypress Element Is Detached From The DOM During Form Input

Some forms replace a submit button as validation changes. Others replace the input itself after each edit. Chaining .type().clear().type() assumes each action leaves the same input in the document. It may work until a new validation rule, framework key, or async state update removes it. The Cypress retry guide recommends a separate query for each action when replacement is possible.

The demo replaces Submit when Name receives input. Query the submit button after typing and assert it became enabled. Then click and assert the submission result. This is stronger than asserting the button alone: it tests the transition and the user's outcome.

it('uses the current submit control after validation', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=name]').type('Ava')
  cy.get('[data-cy=submit]').should('be.enabled')
  cy.get('[data-cy=submit]').click()
  cy.get('[data-cy=form-status]').should('have.text', 'Submitted')
})

Verify with npx cypress run --spec cypress/e2e/detached.cy.js. Expect the test to pass without a fixed delay. If typing itself fails because the input is replaced between keystrokes, inspect the app's controlled component. A user may lose focus for the same reason. Do not try to solve every broken input with shorter strings or a larger timeout; preserve the input node across ordinary changes when the product should allow continuous typing. Cypress selector practices help you identify a control without tying the test to styling classes.

3. Drop a Captured jQuery Subject After Refresh

cy.then() runs its callback once. If you save its $el, then refresh the region and later call cy.wrap($el).click(), wrapping does not re-query the DOM. You are still holding the removed element. This bug is subtle because the code looks like it is returning to the Cypress chain. Cypress documents that a .then() callback does not gain query retry behavior by wrapping its captured element.

Use the original selector at the time of the second action. The Target button in the demo retains an inline handler through cloning, so the status assertion proves that Cypress clicked the current clone. The Refresh target action is a stand-in for a component rerender, cache invalidation, or subscription update.

it('replaces a stored DOM object with a fresh query', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=cached]').should('exist')
  cy.get('[data-cy=refresh]').click()
  cy.get('[data-cy=cached]').click()
  cy.get('[data-cy=cache-status]')
    .should('have.text', 'Clicked current target')
})

Run npx cypress run --spec cypress/e2e/detached.cy.js. If your helper accepts a jQuery object, change its contract to accept a selector or a function that begins a new Cypress query. Keep DOM snapshots only for immediate, read-only inspection. The Cypress query commands tutorial explains where reusable queries fit, while cy.wrap and cy.then examples clarify when a callback is useful.

4. Wait for a Response and the Rendered Ready State

A loading placeholder may be replaced after a fetch. Intercept the request before clicking Load items, or Cypress might miss it. Waiting for the alias proves that the browser request finished; the Ready and list assertions prove the application processed it. A response can complete before React or another framework commits the resulting DOM, so keep the UI assertion.

The demo's request is deliberately stubbed. The response is an array of item names, and the expected list is the user-visible contract. Match the method as well as the path so an unrelated request cannot satisfy the alias.

it('waits for data and the list replacement', () => {
  cy.intercept('GET', '/api/items', ['Alpha', 'Beta']).as('items')
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=load]').click()
  cy.wait('@items').its('response.statusCode').should('eq', 200)
  cy.get('[data-cy=load-status]').should('have.text', 'Ready')
  cy.get('[data-cy=items] li').should('have.length', 2)
  cy.get('[data-cy=items] li').first().should('have.text', 'Alpha')
})

Check it with npx cypress run --spec cypress/e2e/detached.cy.js. If @items never resolves, inspect the actual request URL and method in the Cypress Command Log. A broad **/api/* matcher can hide a wrong endpoint; use a precise matcher during diagnosis. If the alias completes but Ready never appears, investigate rendering or response shape rather than adding a sleep. See cy.intercept examples and waiting with aliases for more request patterns.

5. Locate a Rebuilt List Row by Record Identity

A sort, filter, or virtualized list can rebuild rows. A positional selector such as li:first may select the wrong record after reorder even when it avoids a detached-element failure. That is worse than a visible error because the test can pass after acting on another item. Use a stable ID exposed by the UI or a unique row label, then find the button within that row.

The demo's sort reverses the two rows and reconstructs their markup. The test verifies that Beta is now first, then chooses record a by data-id. In a production app, a stable data-cy attribute on the row can carry the record ID. If IDs are sensitive or unsuitable for the rendered markup, scope by a unique accessible cell value instead.

it('chooses the intended record after rows are rebuilt', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=sort]').click()
  cy.get('[data-cy=rows] li').first().should('contain.text', 'Beta')
  cy.get('[data-cy=rows] li[data-id="a"] button').click()
  cy.get('[data-cy=selection]').should('have.text', 'a')
})

Run npx cypress run --spec cypress/e2e/detached.cy.js. The final assertion is essential: a passing click alone would not reveal a wrong-row selection. When a list is backed by a request, combine the request alias with the identity check. When it is virtualized, scroll until the desired record is rendered, then query it; do not keep an old row element while the viewport moves. The infinite-scroll testing guide explores that separate rendering case.

6. Query a Dialog Again After Close and Reopen

Closing a dialog often unmounts its buttons. Opening it again creates new buttons with identical names. A $apply captured during the first opening cannot be used in the second. Even a selector scoped to a previous jQuery dialog object is stale; start from cy.get('[data-cy=dialog]') after reopening.

The demo clears dialog children on close and builds fresh children on open. The test asserts the closed state before reopening, which makes the transition explicit. Then it queries the new Apply button and checks the result outside the dialog so the assertion remains valid after later closures.

it('acts inside the reopened dialog', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=open]').click()
  cy.get('[data-cy=dialog] [data-cy=close]').click()
  cy.get('[data-cy=dialog]').should('not.be.visible')
  cy.get('[data-cy=open]').click()
  cy.get('[data-cy=dialog]').should('be.visible')
  cy.get('[data-cy=dialog] [data-cy=apply]').click()
  cy.get('[data-cy=dialog-status]')
    .should('have.text', 'Applied in current dialog')
})

Verify with npx cypress run --spec cypress/e2e/detached.cy.js. If the dialog animates, should('be.visible') waits for a state users can observe. An animation delay is not a reliable substitute for that assertion. For portals, check whether the dialog is mounted under body rather than inside the trigger's parent. Query from the document and scope inside the current dialog, not from a node saved before the portal changed.

7. Restart Queries After an Assertion Boundary

An assertion in the middle of a chain can lock in its subject after it passes. A later .find() may then retry from that old subject if a render occurs. This differs from a post-click failure: no action needs to occur in the same line. The app can update between Cypress commands, especially when a subscription or timer refreshes a region.

Split assertions and later traversal into separate document-rooted queries. Here the first assertion checks a row, the sort rebuilds the list, and the second query starts from the current list. The example makes the refresh explicit so it can be verified deterministically.

it('starts a new row query after an assertion and refresh', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=rows] li[data-id="a"]')
    .should('contain.text', 'Alpha')
  cy.get('[data-cy=sort]').click()
  cy.get('[data-cy=rows] li[data-id="a"]')
    .find('button')
    .should('have.text', 'Choose')
  cy.get('[data-cy=rows] li[data-id="a"] button').click()
  cy.get('[data-cy=selection]').should('have.text', 'a')
})

Run npx cypress run --spec cypress/e2e/detached.cy.js. This test should pass even though the Alpha row was replaced. The Cypress should documentation explains the locked-subject behavior. Keep an assertion chain together when it observes a stable node; split it when the next operation depends on a fresh DOM after a known state change. A callback passed to .should() may be retried, so do not put side effects such as clicking or writing data inside that callback.

8. Repair an Application That Continually Remounts the Target

Fresh selectors are enough for one-time replacement, but an action can still fail if the app never settles. Examples include a changing React key on a control's ancestor, an interval that replaces the form on every tick, or a stale response that repeatedly restores a loading state. This can harm a real user too: focus disappears, typed text is lost, or a click lands while the control is being rebuilt.

The demo offers a single Remount target action. The test intentionally waits until that transition has happened, then queries the current Stable action button and verifies its handler. It models the contract you want after a legitimate one-time remount. If your real app remounts continuously, fix the component or expose a meaningful ready state before copying this pattern.

it('acts after an intentional remount has settled', () => {
  cy.visit('/detached-demo.html')
  cy.get('[data-cy=remount]').click()
  cy.get('[data-cy=stable]').should('be.visible').click()
  cy.get('[data-cy=stable-status]')
    .should('have.text', 'Stable action worked')
})

Verify with npx cypress run --spec cypress/e2e/detached.cy.js. If a similar test fails intermittently despite a fresh query, inspect the application's render path. Preserve a stable key for the same logical record, avoid replacing an ancestor on every poll, and reject outdated async results. Check whether the button remains focused during ordinary input. Increasing defaultCommandTimeout may make Cypress wait longer, but it will not turn a perpetual remount into a usable interface.

CI and Docker Variants

CI scheduling can widen a race that is hard to reproduce locally. Run the focused spec on the same browser family and against the same application state as the failing job. Keep the server process alive before launching Cypress. For the scratch demo, this shell sequence starts the Python server, waits until the page responds, runs the spec, and stops the server when the shell exits:

python3 -m http.server 4173 --directory site &
server_pid=$!
trap 'kill "$server_pid"' EXIT
until curl -fsS http://127.0.0.1:4173/detached-demo.html >/dev/null; do sleep 1; done
npx cypress run --spec cypress/e2e/detached.cy.js

Use this command from the scratch directory after installing dependencies and saving the demo and spec. A successful exit verifies all included tests. In a real CI pipeline, retain the failed test's screenshot and video if your Cypress configuration records them. Compare the Command Log with the server response and the app's render sequence. A passing retry is diagnostic evidence of timing sensitivity, not proof that the old subject is safe. See handling Cypress flaky tests for how to organize that investigation.

For Docker, use a Cypress included image tag that matches your installed Cypress version. The placeholder is intentional. Keep the Python server running on the host, then run the container from the scratch directory. The host mapping lets the container reach the server without treating its own localhost as the host:

npx cypress --version
docker run --rm --add-host=host.docker.internal:host-gateway \
  -v "$PWD:/work" -w /work \
  cypress/included:<your-cypress-version> \
  --config baseUrl=http://host.docker.internal:4173 \
  --spec cypress/e2e/detached.cy.js

Replace the tag with a published image for your installed version. The cypress/included image already starts the Cypress runner, so the arguments after its tag are runner options. If your actual app runs on another host or port, set baseUrl to a URL reachable from the container. Docker does not alter Cypress's DOM subject semantics, so the same chain fixes still apply.

How to Verify the Fix

First, run the single spec and read each failing command. The runnable demo has one test per cause. Each passes only after a post-action state is asserted. For your production test, keep the failed screenshot or video, make the smallest cause-specific change, and run the original user journey. Do not declare success because a re-queried button exists if the required save, deletion, or navigation never happened.

Second, repeat the focused spec several times with npx cypress run --spec cypress/e2e/detached.cy.js --config retries=0 in your shell loop or CI matrix. retries=0 is a documented configuration override and leaves each failure visible during diagnosis. Record the failing command and app state rather than citing an illustrative repeat count as a reliability guarantee. The single-test execution guide can help isolate one it block in a larger suite.

Third, inspect the app's lifecycle. In the browser console, a temporary MutationObserver can show whether the target or its ancestor is repeatedly removed. In Cypress, a cy.get(selector).should('be.visible') proves visibility at the assertion point, not permanent attachment. Place the next action adjacent to the ready-state assertion and keep the outcome check. If the test still fails, look for a subsequent render that invalidates the subject.

Finally, run the repaired test where the error originally appeared. Match viewport, feature flags, browser, and response behavior when those affect rendering. If the issue occurs only with a real API, a stubbed demo proves the Cypress pattern but does not verify the production path. Keep one test with the real response, or a contract check for the stub's shape, so the ready-state assertion cannot silently become meaningless.

Prevent It From Coming Back

Review Cypress helpers for methods that return $el, store HTMLElement, or call cy.wrap() long after the first query. Return a selector-based query instead. Keep actions at the end of a chain when they may mutate the page. Name selectors for user intent or stable data-cy attributes, and scope repeated controls by dialog or record identity.

Treat readiness as an application contract. A loading label, enabled submit button, completed request, or rendered record gives both users and tests a recognizable boundary. Register intercepts before the action, then assert the UI after cy.wait('@alias'). Avoid fixed sleeps, generic retries, and forced clicks as standard synchronization. They can make a build greener while leaving duplicate submissions or focus loss unresolved.

For components you own, maintain stable keys and avoid remounting entire forms for minor text changes. Cancel or ignore stale fetch results that would overwrite a newer view. Add one regression test for the exact transition that previously detached the subject. The Cypress network-stubbing guide can make that transition deterministic, while a separate live-response test checks the integration path.

Interview Questions and Answers

Q: What does a detached DOM subject mean in Cypress? The yielded element was removed from the active document before a later command used it. A visually identical replacement is a different node.

Q: Why can cy.get() help when .click() cannot retry? A new cy.get() starts a query from the current document. Cypress does not replay the earlier click because an action can have non-idempotent effects.

Q: Where should a chain end? End it after an action that can update the target or its ancestors. Query again before assertions or actions that depend on the new page state.

Q: Can cy.wrap($el) refresh a stale element? No. It wraps the same captured jQuery object. Use a selector query or a helper that starts a new query.

Q: What does a network alias prove? It proves a matching request and response cycle completed. Assert the UI separately because the application may render later or reject the response.

Q: When is this a product bug? Repeated remounts that interrupt focus, typing, or clicks are a user-visible defect. Repair component identity or state handling rather than only adjusting the test.

Common Mistakes

  • Chaining .click().should('have.class', ...) when the click replaces the button. Assert a fresh node or the outcome instead.
  • Assuming .should('be.visible') guarantees future attachment. It describes the node at that assertion point.
  • Using cy.wrap($el) after a list refresh. It preserves the old object.
  • Calling cy.wait(1000) without identifying the render boundary. It only delays the race.
  • Using { force: true } for a detached node. Force relaxes actionability checks; it does not reconnect a removed element.
  • Selecting .first() after a sort when the test means a particular record. Query by stable identity.
  • Waiting for an API alias registered after the click. The request may already be gone.
  • Treating every failed click as detachment. Read the actual Cypress message; covered, disabled, and animating controls need different fixes.

Conclusion

Fix Cypress Element Is Detached From The DOM by identifying the state change that removed the subject, ending the old chain, and querying the current DOM. Pair the new query with a ready-state assertion and a user-visible outcome. If the page continuously replaces a control, repair the application lifecycle, then rerun the original test in the environment that exposed the failure.

Interview Questions and Answers

How do Cypress queries differ from action commands in retry behavior?

Queries such as cy.get can re-run while resolving a subject. An action such as click executes once because repeating it could change application state twice. I start a new query after an action that can replace its target.

What evidence would you gather for a detached DOM failure?

I would locate the first failed command in the Command Log and identify the preceding click, input, response, or route change. I would compare the old subject with the current rendered control and check whether the app replaced it once or repeatedly. Then I would test a cause-specific change.

Why might click followed by should fail even when click succeeds?

The click can remove the element it acted on. The subsequent assertion may receive that old element instead of the replacement. I would assert the resulting UI through a new query and verify the business outcome.

How would you fix a stale element saved in a then callback?

The callback and its jQuery object are not retried. I would keep a stable selector or query helper and call cy.get again after the refresh. I would avoid cy.wrap on the saved object after any render boundary.

What should happen after cy.wait on an intercepted request?

I would assert the response property that matters and then query the UI for its ready state. A completed response does not guarantee the framework has rendered it. I would register the intercept before triggering the request.

How do you avoid wrong-row actions after a list rerenders?

I locate the row by a stable record ID or unique visible data after the refresh. Then I find its action within that row and assert which record changed. Positional selectors can silently select another record after sorting.

How would you distinguish a test synchronization issue from an application defect?

A one-time intentional replacement needs a fresh query and a ready-state assertion. Continuous remounting, lost focus, or dropped clicks affects users as well as tests. I would inspect keys and async state updates and fix the component lifecycle if the behavior is unstable.

Would you use retries or force click as the final solution?

Retries can collect evidence about frequency but may hide the underlying race. Force click changes actionability checks and cannot restore a detached node. I would first fix the chain or rendering cause, then verify the actual outcome in CI.

Frequently Asked Questions

What does Element is detached from the DOM mean in Cypress?

Cypress is holding a reference to a node that the application removed. A replacement may look identical, but the yielded subject still points to the old node. Start a new query after the state change.

Why does a Cypress test fail after clicking a button?

The click may cause the button or an ancestor to be replaced. A command chained after that click receives the pre-click subject. End the chain and assert the updated page with a fresh query.

Does cy.get automatically retry a detached subject?

A new cy.get query resolves elements in the current document and can retry while finding them. It cannot make a subject already yielded by an earlier action current. Query again after the action.

Can cy.wrap fix a detached element?

Wrapping a saved jQuery collection preserves that same collection. It does not run the selector again. Use cy.get with a stable selector or a helper that begins a new query.

Should I add cy.wait with a number of milliseconds?

A fixed delay does not prove that the relevant render finished. Wait for a matching aliased request when appropriate, then assert a user-visible ready state. This gives a specific failure if the application never becomes ready.

Will click with force true resolve the detached DOM error?

No. Force changes selected actionability checks but does not attach a removed node. It can also conceal a real interaction problem, so diagnose the render transition first.

Why does the failure occur only in CI?

Different scheduling can expose the interval between an old subject being yielded and the next command. Run the focused test in the CI browser, inspect the failing command, and verify the same state boundary and outcome there.

When should the application code change?

If the target repeatedly remounts during normal interaction or users lose focus and clicks, the UI lifecycle is unstable. Preserve component identity or make the transition explicit, then cover it with a regression test.

Related Guides