Have you ever encountered end-to-end test error "... failed because the page updated as a result of this command, but you tried to continue the command chain. The subject is no longer attached to the DOM"?

The example above shows the test trying to click on the button, yet the command fails.
1 | // the failing / flaky test |
This blog post shows when it happens and how to prevent it.
🎁 The demo application for this blog post can be found in the repo bahmutov/delayed-reload.
Imagine a client-side rendering that refreshes the elements based on the current route. We can confirm the app navigates correctly by checking the currently rendered card.
1 | it.only('navigates from home to about', () => { |
I specifically slowed down route navigation by 1 second to make the application behavior clear.
On each page the user can click the "Like" button which prints a message to the console log.

The "Home" component prints the "I like this" message, the "About" component prints "I like this about page" message. Let's verify the "Home" page works.
1 | it('likes the home page', () => { |

Let's write a test for "About" component and its "Like" button. We get to the "Home" page, navigate to the "About" page, and press "Like". Sounds easy.
1 | it('likes the about page (does not work)', () => { |
Hmm, the test fails.
The test does make sense: click on the "About" link, wait for the location to be "/about", then find the "Like" button and click on it. Yet Cypress complains that the "click" command failed because the "Like" button ... detaches from DOM? How can this be?
Look at the video closely. Notice that the URL part changes to "/about" immediately after the cy.contains('a', 'About').click() commands. Yet the component does not refresh instantly. Instead, you still see the old "Home" component. The navigation and refresh is delayed. We cannot simply rely on the following assertion to detect when the application finishes reloading a page or part of it
1 | // NOT ENOUGH TO CHECK |
If the old page is still visible, finding the "Like" button is dangerous - we might find the "Like" button from the old page and try pressing it. But that button is gone by this point and Cypress correctly fails the test.
Fixing the test
There are several solutions to the problem. The easiest and clearest code would confirm the page has finished loading and rendering in response to the navigation before finding the "Like" button.
1 | it('likes the about page (fixed)', () => { |
Here is how it works - notice that the test retries until the "About" component is visible before finding the "Like" button.
So my advice is to not simply rely on cy.location(...).should(...) assertions and instead check the DOM before proceeding.
Element reloads fix
There is another way to fix the test. We can check if the "card" element changes in response to the navigation. Since we will compare the element references (old and new), we need to "prepare" before clicking. Here is one possible implementation
1 | Cypress.Commands.add('prepareForReload', (selector) => { |
The important part are commands surrounding the click() action
1 | cy.prepareForReload('.card') |
Element detaches fix
Another possible fix is possible assuming the component is re-rendered only after some time after clicking on the "Like" button. Here is the solution that checks if an element is detached from the DOM using Cypress.dom.isDetached utility function.
1 | it('likes the about page (check the element detaches)', () => { |
Here is how it looks in the Test Runner
Of course, if the element is immediately detached on click, the above solution would not work.