skip to content

Reusable Chain Steps

Adding your own steps to the cy chain: parent, child or dual commands, overwriting a built-in, and the separate API a retrying query needs. Interviewers probe the prevSubject option.

on this pageshow

explore

questions

5

In Cypress, what does the `prevSubject` option in `Cypress.Commands.add()` control?

level: juniorimportance: must knowfreq 72%

answer

  1. The second argument to the add call
  2. Parent, child or dual
  3. Decides who receives the previous subject
  4. false, true, or the string optional
  5. element, document and window also validate

basics

~20 s

It decides how your command treats the subject the previous command yielded: false makes a parent that ignores it, true a child that receives it, optional a dual command that works either way. element, document and window also validate it.

solid answer

~40 s

`Cypress.Commands.add(name, options, callbackFn)` reads `prevSubject` from its options object, and that one key does two jobs. It sets the command's **type**: `false`, the default, makes a **parent** command that starts a fresh chain and discards anything before it; `true` makes a **child** command that must be chained and receives the previous subject as the callback's first parameter, shifting your own arguments one place right; `'optional'` makes a **dual** command that works both ways, so the subject may be `undefined` and the body has to branch. It also declares **subject validation**: `'element'`, `'document'` and `'window'` make Cypress check the incoming subject and fail the command before your callback runs. The values combine in an array and are OR'd, which is how the built-in `.scrollTo()` is declared as `{ prevSubject: ['optional', 'element', 'window'] }`.

code

javascript · 17 lines
javascript
// cypress/support/commands.js
Cypress.Commands.add(
  'approve',
  { prevSubject: 'element' },
  (subject, approverNote) => {
    // subject arrives first; approverNote is shifted one place right
    cy.wrap(subject).find('[data-cy=approver-note]').type(approverNote)
    cy.wrap(subject).find('[data-cy=approve-report]').click()

    return cy.wrap(subject)
  }
)

// cypress/e2e/expense-reports.cy.js
cy.get('[data-cy=report-R-91]')
  .approve('Within travel policy')
  .should('contain', 'Approved')

go deeper

for a junior

Be ready to recite the three values and what each one makes: false a parent command, true a child, optional a dual. Also remember that a child command's callback receives the subject before your own arguments.

for a middle

Explain that prevSubject sets the command type and declares subject validation at the same time, and that element, document and window are OR'd when given as an array.

for a senior

Show that you pick the narrowest validation a helper can carry so a misuse fails on the calling line with a readable error rather than deep inside a shared support file.

for a principal

Own the convention: which shared steps are allowed to be child commands at all, and how the team keeps subject rules and the TypeScript declarations for them from drifting apart.

## The option and where it lives `Cypress.Commands.add()` accepts either `(name, callbackFn)` or `(name, options, callbackFn)`, and the documented key in that options object is **`prevSubject`**, whose default is `false`. `Cypress.Commands.addAll()` takes the same options object and applies it to every command in the object you hand it, so a whole family of expense-report steps can share one subject rule in a single call. `Cypress.Commands.overwrite()`, by contrast, accepts **no** options object — an overwrite inherits the subject rules of the command it replaces. `prevSubject` answers exactly one question: *what should Cypress do with the subject the previous command yielded, before it calls my callback?* The answer settles two separate things — the command's **type**, and whether Cypress **validates** the subject's shape first. ## Parent, child and dual | `prevSubject` | Command type | How it chains | Callback signature | |---|---|---|---| | `false` (default) | parent | always begins a new chain | `(...yourArgs)` | | `true` | child | must follow another command | `(subject, ...yourArgs)` | | `'optional'` | dual | works with or without a subject | `(subjectOrUndefined, ...yourArgs)` | - A **parent** command behaves like `cy.visit()` or `cy.request()`. If you write `cy.get('[data-cy=report-R-91]').seedReceipts()` and `seedReceipts` is a parent command, the report row is silently thrown away and the command starts over on its own. - A **child** command behaves like `.click()`. Cypress passes the previous subject in as the callback's first parameter and **shifts your own arguments one place right** — the detail candidates most often get wrong when reading a support file. Invoking it with nothing in front of it fails immediately with an error saying a child command must be chained after a parent, because it operates on a previous subject. - A **dual** command behaves like `cy.scrollTo()` or `cy.screenshot()`. `subject` may be `undefined`, so the body branches on it. The Cypress documentation calls dual commands rare and says only a handful of built-ins use the form; reach for one only when the step genuinely reads well both ways. ## Validation is a second axis `'element'`, `'document'` and `'window'` are **validations**, not types. Passing one of them implies the command takes a subject, and additionally tells Cypress what that subject must be: - `'element'` requires one or more DOM elements, and also checks that the element is still attached to the document. A mismatch fails with a message of the form *"cy.markPaid() failed because it requires a DOM element"*, raised before your callback body executes. - `'document'` and `'window'` require the document or the window object respectively — the subjects that `cy.document()` and `cy.window()` yield. - Values combine in an array and are evaluated as **or**, never **and**: `{ prevSubject: ['element', 'document', 'window'] }` is how the built-in `.trigger()` is declared, and `{ prevSubject: ['optional', 'element', 'window'] }` is how `.scrollTo()` is declared. - `{ prevSubject: true }` on its own requires that *a* subject exists but checks nothing about its type, so your callback still has to cope with whatever arrived. Getting this right is worth the two extra words: a validated command fails on the line that misused it, with a message naming the command, instead of throwing something unhelpful three lines deeper inside a shared helper. ## What the command yields Whatever your callback returns becomes the subject for whatever is chained next. Two patterns cover almost every expense-report helper you will write: 1. **Return the subject unchanged** when the command is a step in a longer chain — the documented `.console()` example ends with `return subject` precisely so the chain carries on untouched. 2. **Return a `cy` chain** when the command should hand on something new: `return cy.wrap(subject).find('[data-cy=approver-note]')` yields that element, so a caller can go straight into `.should('contain', 'Within policy')`. Inside a child command it is usually worth passing the raw subject through `cy.wrap()` before doing anything with it, because that turns a plain jQuery object back into something the rest of the Cypress API can act on. ## Choosing a value 1. If the step is setup that stands on its own — seed a report, open the reimbursement queue — leave `prevSubject` off entirely and get a parent command. 2. If the step only makes sense against something already found on the page — approve *this* row, attach a receipt to *this* claim — use `{ prevSubject: 'element' }` rather than bare `true`, so a wrong subject is rejected with a readable error. 3. Reach for `'optional'` only when both readings are genuinely useful, and branch on `subject` in the first line of the callback so the two paths are obvious to the next reader. 4. Declare the same shape in your TypeScript `Chainable` entry, because Cypress infers the callback's parameter types from that declaration rather than the other way round.

  • What happens if you call a Cypress command registered with `{ prevSubject: true }` as `cy.approve(...)`, with nothing before it?
    Cypress fails the test before your callback runs, reporting that you are trying to call a child command before a parent command and explaining that a child command must be chained because it operates on a previous subject. The fix is to chain it — `cy.get('[data-cy=report-R-91]').approve(...)` — or to re-register the step without `prevSubject` if it never needed a subject.
  • How does `Cypress.Commands.addAll()` treat the `prevSubject` option?
    `Cypress.Commands.addAll(options, callbackObj)` applies the one options object to every command in `callbackObj`, so all of them become parent, child or dual together. It is convenient for a set of row-level expense helpers that share a subject rule, and wrong for a mixed bag — register those separately with `Cypress.Commands.add()` so each gets its own rule.

saying these in an interview costs you the question

  • Says every custom command automatically receives the previous subject
  • Thinks prevSubject can also be passed to Cypress.Commands.overwrite
  • Forgets that a child command shifts its own arguments right
  • Believes element and optional cannot appear in the same array
open as a page

In a TypeScript Cypress project, how do you type a custom `cy.approveReport()`?

level: middleimportance: should knowfreq 58%

basics

~20 s

Add the method to Cypress's global Chainable interface by declaration merging, in the same support file that registers the command. Wrap it in declare global, and make that file a module by adding export {} if it has no imports.

open as a page

In Cypress, what arguments does a `Cypress.Commands.overwrite()` callback receive?

level: middleimportance: should knowfreq 46%

basics

~20 s

First the original implementation as originalFn, then the previous subject if the command being overwritten takes one, then the arguments the test passed. Call originalFn to keep the built-in behaviour; overwrite accepts no options object.

open as a page

In Cypress 16, why does `Cypress.Commands.overwrite('getCookie', fn)` now throw?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Because cy.getCookie() became a retry-able query in Cypress 16. Cypress.Commands.overwrite() replaces commands only, so it refuses the name and tells you to use Cypress.Commands.overwriteQuery() instead, which takes a different callback shape.

open as a page

In Cypress, why must a `Cypress.Commands.addQuery()` callback return another function?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Because the two halves run at different rates. The outer function runs once, for setup; the inner one takes the previous subject and returns the new one, and Cypress invokes it repeatedly, so it must be synchronous and side-effect free.

open as a page