Skip to content

Chainable Commands in Cypress

Command chaining is central to writing concise Cypress tests. This lesson explains what a "subject" is, how it flows from one command to the next, and common chaining patterns.

What Is a Subject?

Every Cypress command yields a subject, a value passed along to the next command in the chain. cy.get('.item') yields the matched elements; .first() yields just the first of those elements; .click() yields the same element it clicked, allowing further chaining.

Some commands change the subject completely (like .find(), which searches within the current subject and yields new elements), while others pass the same subject through unchanged (like most action commands).

cy.get('[data-cy="cart-item"]')
  .first()
  .find('[data-cy="remove-button"]')
  .click()
  .should('not.exist');

Each step's subject: all cart items -> first item -> its remove button -> (clicked) the same button -> then asserted gone.

Chaining Patterns

cy.get(selector)         // subject: matched elements
  .find(childSelector)    // subject: matched children
  .should(assertion)      // subject: unchanged, retried until it passes
  .click()                // subject: same element, now clicked
  • .find() searches for descendants of the current subject, not the whole document.
  • .should() and .and() re-run automatically, retrying until the assertion passes or times out.
  • .click(), .type(), and similar action commands generally yield the same subject unchanged.
  • cy.get() at the start of a chain always searches the whole document, not the previous subject.

Chaining Quick Reference

Common command combinations and how the subject changes at each step.

Chain Resulting Subject
cy.get('.list').find('li') The <li> descendants of .list
cy.get('.list').children() Direct child elements of .list
cy.get('.item').first() Just the first matched .item
cy.get('.item').eq(2) The third matched .item (zero-indexed)
cy.get('input').clear().type('x') The input, now cleared and typed into
cy.contains('Save').parent() The parent element of the text match

Starting a New Chain vs. Continuing One

A new cy.get() or cy.visit() always begins a fresh chain against the whole document, it deliberately ignores whatever subject an earlier chain produced. To operate relative to a previous subject, use .find(), .children(), .parent(), or similar traversal commands instead of a second cy.get().

// Wrong: second cy.get() searches the whole document again
cy.get('[data-cy="card"]');
cy.get('.title'); // not scoped to the card above

// Right: .find() stays scoped to the previous subject
cy.get('[data-cy="card"]').find('.title');

Why Cypress Re-Queries Before Retrying

When a .should() assertion fails, Cypress does not just re-check the same DOM element, it re-runs the entire chain leading up to that point, including cy.get(). This matters because it means retries can correctly handle elements that get removed and re-added to the DOM (a common pattern after data reloads).

  • Assertions retry the whole preceding query chain, not just the final check.
  • This is why chaining directly (cy.get(x).should(y)) is more resilient than splitting across separate statements.
  • Storing an element reference in a variable defeats this re-querying behavior.

Common Mistakes

  • Calling cy.get() a second time expecting it to search within a previous subject instead of the whole document.
  • Saving a jQuery element reference to a variable and reusing it after the DOM has changed underneath it.
  • Splitting a query and its assertion across separate cy.get() calls, losing automatic re-querying on retry.
  • Chaining .should() after an action command in a way that checks stale state instead of the post-action result.

Key Takeaways

  • Every command yields a subject; some commands transform it, others pass it through unchanged.
  • cy.get() and cy.visit() always start a fresh chain against the whole document.
  • Traversal commands like .find(), .parent(), and .children() operate relative to the current subject.
  • Assertions retry the entire preceding chain, not just the final check, which is why direct chaining is more resilient.

Pro Tip

Keep queries and their assertions in a single chain (cy.get(x).should(y)) rather than splitting them across multiple statements, this preserves Cypress's automatic re-querying behavior and avoids a whole category of flaky tests.