skip to content

In Cypress, why can .click().should('be.disabled') fail once the button's row re-renders?

level: seniorimportance: should knowfreq 52%

answer

  1. Where does a chain stop retrying?
  2. One kind of step never relinks
  3. The subject is fixed at the action
  4. Detached because the page updated
  5. Split the statement, query again

basics

~20 s

Because an action fires once and cannot relink. Cypress will not re-run the queries in front of .click(), so the chained .should() gets the exact node the click landed on — and a re-render leaves that node detached.

solid answer

~50 s

`.click()` is an action, and Cypress will not re-run any query that came before an action once it has fired. So in `cy.get('.book-row').contains('button', 'Borrow').click().should('be.disabled')`, the subject handed to `.should()` is the exact DOM node the click landed on. If the catalogue re-renders that row in response to the borrow, the node is replaced and the assertion fails with a detached-subject error saying that the page updated as a result of this command and Cypress cannot requery after it. The failure has nothing to do with the selector or the assertion — it is the chain's shape. End the statement at the action and start a new one, so a fresh `cy.get()` relinks the queries against the new DOM. As a rule, an action belongs at the end of a Cypress chain, never in the middle of one.

code

javascript · 9 lines
javascript
// BAD: the action is mid-chain, so nothing before it can relink
cy.get('[data-cy=book-row-dune]')
  .contains('button', 'Borrow')
  .click()
  .should('be.disabled')

// GOOD: end the chain at the action, then query again from the top
cy.get('[data-cy=book-row-dune]').contains('button', 'Borrow').click()
cy.get('[data-cy=book-row-dune]').contains('button', 'Borrow').should('be.disabled')

go deeper

for a junior

Remember the shape rule before the theory: one action per statement, and assert on the result in the next statement starting from cy.get() again.

for a middle

Explain why Cypress refuses to requery past an action, and contrast it with the way the same queries are replayed while the action waits to become actionable.

for a senior

Diagnose the real failure from the detached-subject message — that the click worked and the chain outlived its subject — and fix the chain rather than reaching for a longer timeout.

for a principal

Set the review standard that keeps this class of flake out of the suite, and decide how the team handles the components whose re-render behaviour makes chained assertions unsafe.

## What "an action does not relink" means Cypress relinks a chain of queries by re-running it from the top, discarding the elements it found last time. Actions are excluded from that. An action changes the application, so replaying it would change the test, and Cypress therefore does two things at an action boundary: it runs the action exactly once, and it refuses to re-run anything in front of it afterwards. That refusal is what breaks a chain like this one in a library catalogue spec: ```javascript cy.get('[data-cy=book-row-dune]') .contains('button', 'Borrow') .click() .should('be.disabled') ``` `.click()` yields back the element it clicked. `.should('be.disabled')` then judges that element. If borrowing causes the framework to re-render the row, the button object the assertion holds is no longer in the document — and Cypress cannot go back and re-resolve it, because doing so would mean replaying `cy.get()` and `.contains()` past an action. ## The error you actually see Cypress reports this precisely rather than as a generic timeout. The message says that the command failed **because the page updated as a result of this command, but you tried to continue the command chain**, that the subject is no longer attached to the DOM, and that Cypress cannot requery the page after commands such as the one named. It then lists the usual causes: - The JavaScript framework re-rendered asynchronously. - The application reacted to the event and removed or replaced the element. Reading that message correctly is the senior skill here. It is not telling you the selector is wrong or that the element never existed; it is telling you the element existed, the action worked, and the chain then outlived its subject. ## Actions do re-run the queries in front of them — beforehand The nuance that makes this worth understanding rather than memorising: an action is not indifferent to a changing page *before* it fires. While `.click()` waits for the element to become actionable, Cypress keeps re-running the query chain leading up to it, so the element it finally acts on is freshly resolved. If the element disappears during that wait you get a different, clearly worded failure about the page updating while the command was executing. So the timeline is: 1. Queries in front of the action run and produce a subject. 2. The action waits for actionability, replaying those queries as the page changes. 3. The action fires — once. 4. From here on, nothing before the action is replayed. The subject is fixed. ## Actions belong at the end of a chain The fix is structural, not a matter of timeouts or waiting harder. Break the statement at the action so the next assertion begins a fresh chain of queries that relink from the top: | chain shape | what can relink | risk | |---|---|---| | `cy.get(...).contains(...).click().should(...)` | nothing after the click | assertion sees a possibly detached node | | `cy.get(...).contains(...).click()` then `cy.get(...).contains(...).should(...)` | the whole second chain | none — the row is re-found | | `cy.get(...).click().click()` | nothing after the first click | second click acts on a stale element | The same reasoning applies to several actions in a row. Chaining `.focus().clear().type('dune')` on one element is fine when the node is stable, because each of those actions yields back the element it received. When the application can replace the node between them, issue a separate `cy.get()` per action instead: Cypress re-runs all the queries leading up to each action, so every action gets a freshly resolved element. ## A working rule - Put exactly one action at the end of a statement. - Put assertions about the result of that action in the next statement, starting from `cy.get()` again. - Treat a chain with an action in the middle as a defect to fix during review, not a flake to retry in CI. - When a detached-subject error appears, look at the chain's shape first; the selector is almost never the cause. Splitting a statement costs a second `cy.get()`, which is a re-read of the DOM and nothing more. It is far cheaper than the alternative, which is a test that passes on a fast machine and fails in CI because the borrow response landed a few milliseconds earlier and the row was rebuilt before the assertion ran. A chain that ends at its action is deterministic regardless of how quickly the application responds to it.

  • In Cypress, does an action ever re-run the queries in front of it?
    Yes, but only before it fires. While `.click()` waits for the element to become actionable it keeps re-running the query chain leading up to it, so it acts on a freshly resolved element. Once the click has happened, nothing in front of it is replayed again.
  • In Cypress, is it safe to chain .focus().clear().type('dune') on one element?
    Yes while the node is stable — each of those actions yields back the element it received. If the application can replace the node between them, issue a separate `cy.get()` per action so the queries relink and every action operates on an element resolved a moment earlier.

saying these in an interview costs you the question

  • Thinks Cypress retries a .click() like cy.get()
  • Blames the selector when the node was replaced
  • Believes the subject is re-resolved after every command
  • Chains several actions and assertions off one cy.get()
  • Confuses a detached subject with a missing element