skip to content

Which shared Cypress helpers should be registered with Cypress.Commands.addQuery()?

level: principalimportance: should knowfreq 36%

answer

  1. Not everything deserves an abstraction
  2. Three rules decide the kind
  3. Could it run twice harmlessly?
  4. A command ends the retry chain
  5. Synchronous, retriable, idempotent

basics

~20 s

Only helpers that are a pure, synchronous, repeatable read of the page or browser state. Anything that acts, awaits, or drives other cy commands must be a command — and a command stops the chain relinking after it.

solid answer

~50 s

Register a helper as a query when it meets the three rules Cypress requires of one: it is **synchronous**, returning no promise; it is **retriable**, so Cypress owns the loop; and it is **idempotent**, so running it repeatedly cannot change the application. A helper like `cy.bookRow(title)` that merely narrows the catalogue table qualifies, and because it is a query it links into the chain, so everything after it keeps relinking when the list re-renders. A helper that borrows a book, seeds a loan through `cy.request()`, or awaits anything must be a command registered with `Cypress.Commands.add()`, and the chain stops relinking there. The judgement is how much of the team's shared vocabulary sits on the retriable side of that line — while remembering Cypress's own advice not to make everything a query, because a helper wrapping one selector buys nothing and costs the reader an indirection.

go deeper

for a junior

Know that Cypress lets you register your own steps, and that the three-rule contract for a query is why some helpers cannot be one.

for a middle

Explain the outer-and-inner function shape and why idempotence matters when Cypress calls the inner function repeatedly during a retry loop.

for a senior

Show how you would audit an existing support file: which helpers are silently ending the retry chain, and which of those could safely be queries instead.

for a principal

Own the standard itself — how much shared vocabulary is worth registering at all, which helpers earn query status, and how the team records the kind so callers know whether their assertions retry.

## The three rules that decide the kind Cypress states the contract for a query plainly, and it is a contract rather than a style preference: 1. **Synchronous.** A query does not return or await a promise. If your helper has to wait on anything — a network call, a file read, a Node task — it cannot be a query. 2. **Retriable.** Once the outer function returns the inner function, Cypress owns the loop and calls that inner function as often as it likes. 3. **Idempotent.** Because the inner function is invoked repeatedly, invoking it must not change the state of the application under test. Rule three is the one that decides most real cases. A helper that finds the row for a given book title in a library catalogue can run fifty times with no consequence. A helper that clicks **Borrow** cannot run twice without borrowing twice. ## What the kind actually buys, and costs | helper | kind | why | effect on the chain | |---|---|---|---| | `cy.bookRow('Dune')` — narrows the results table | query | pure, synchronous read | links in; everything after it relinks | | `cy.dueDateFor('Dune')` — reads a cell's text | query | pure, synchronous read | links in; assertions after it retry | | `cy.borrowBook('Dune')` — clicks through the flow | command | performs actions | chain stops relinking at it | | `cy.seedLoan({...})` — posts through `cy.request()` | command | asynchronous, side-effecting | chain stops relinking at it | The upside of a query is inherited retry-ability: a `.should()` written after `cy.bookRow('Dune')` re-runs the helper itself, so a catalogue that re-renders is handled without the caller thinking about it. The cost of a command is the mirror image — the linked run of queries ends there, so anything the caller chains afterwards starts from whatever subject the command yielded and cannot be re-derived. ## The cases that look like queries and are not - **Anything that calls other `cy` commands inside it.** Queries are plain synchronous functions of a subject; a helper that composes `cy.get()` and `.click()` is a command by construction. - **Anything that reads and then caches.** If the second invocation returns something different from the first for reasons of your own bookkeeping, the helper is not idempotent and will misbehave inside a retry loop. - **Anything that logs, counts, or records.** Side effects in the inner function run once per attempt, not once per call, so counters and one-shot setup belong in the outer function or nowhere. - **Anything that must fail fast.** A query's failure is a retry, so a helper whose whole purpose is an immediate negative check fits badly. ## Setting the standard for a team The open decision a lead owns is how much shared vocabulary to express as queries at all. Cypress's own guidance is explicitly conservative: do not make everything a custom query, because every abstraction over `cy.get(selector)` costs a reader an indirection for no retry benefit that the plain selector did not already have. `cy.getBorrowButton()` wrapping a single `cy.get()` is worse than the selector it hides. A defensible standard looks like this: 1. Express a helper as a **plain JavaScript function** first, if it is only building data or computing an expected value. Test code is JavaScript, and a function is the cheapest thing to read. 2. Promote it to a **query** only when it is a real page or browser read that several specs share, and when callers will want to chain assertions that must retry through it. 3. Make it a **command** when it acts, awaits, or orchestrates — and accept, deliberately, that the chain stops relinking there. 4. Write down which kind each shared helper is, because the kind is invisible at the call site and determines whether the caller's assertions retry. ## The judgement in one line Ask what happens if Cypress runs this helper five times in a row while waiting for the page to settle. If the honest answer is "nothing, it just reads the page again", it is a query. If the answer involves the application changing, it is a command, and the cost of that choice is that everything chained after it loses the ability to re-resolve.

  • What happens to retrying when a custom Cypress command sits in the middle of a chain?
    The linked run of queries stops at it. Queries before the command are not replayed once it has run, and queries after it begin a fresh chain from whatever subject the command yielded. That loss of relinking is the main cost of choosing a command over a query.
  • In Cypress, when is a plain JavaScript function better than either a query or a command?
    When the behaviour is local to one spec, or is not a read of the page at all — building fixture data, computing an expected due date, formatting a title. Cypress's own guidance is that test code is JavaScript, and a function is cheaper to read than a registered abstraction.

saying these in an interview costs you the question

  • Wraps every selector in a custom query
  • Puts a cy.request() call inside a query function
  • Assumes a custom command retries like cy.get()
  • Registers cy.getBorrowButton() for one cy.get() call
  • Forgets the inner function runs many times per call