skip to content

Fixtures and Custom Commands

The reuse layer of a Cypress suite: canned data files, state seeded outside the browser, custom chain steps and cached logins. Interviewers probe where a team draws that line.

on this pageshow

explore

questions

25

Cypress 16 removed cy.exec() - how do you run a setup command now?

level: juniorimportance: must knowfreq 58%

answer

  1. The shell door closed in version 16
  2. Setup now runs in Cypress's Node process
  3. One command replaced by an event name
  4. cy.task supersedes the removed cy.exec
  5. execTimeout became taskTimeout, 60000 ms

basics

~20 s

cy.exec() was removed in Cypress 16 and now throws; use cy.task() instead, calling a handler registered under the task event in setupNodeEvents. The execTimeout option was removed with it, and taskTimeout, default 60000 ms, replaces it.

solid answer

~50 s

`cy.exec()` was removed in Cypress 16.0.0 after being deprecated in 15.21.0; calling it throws an error pointing you at `cy.task()`. `cy.exec()` shelled out on the machine running Cypress, so its behaviour depended on the operating system, the shell, the login environment and whether a terminal was attached. `cy.task(event, arg, options)` instead invokes a handler registered under the `task` event in `setupNodeEvents`, running in the Node process that loaded your Cypress config - no shell resolution, no per-platform quoting. It also hands back a real value rather than stdout you have to parse, so `cy.task('seedExpenseReport', { total: 42.5 })` can yield the created report object. The handler must return a value or `null`; `undefined` fails the command. `execTimeout` went with the command - setting it only prints a warning - and `taskTimeout` (60000 ms) bounds `cy.task()`.

code

javascript · 15 lines
javascript
// Removed in Cypress 16 - this now throws:
// cy.exec('node scripts/seed-expenses.js').its('stdout')

describe('expense report detail', () => {
  beforeEach(() => {
    cy.task('resetExpenseLedger')
    cy.task('seedExpenseReport', { employee: 'jane', total: 42.5 }).then((report) => {
      cy.visit(`/reports/${report.id}`)
    })
  })

  it('shows the submitted receipt total', () => {
    cy.get('[data-cy="report-total"]').should('have.text', '$42.50')
  })
})

go deeper

for a junior

Recall that cy.exec() is gone in Cypress 16 and cy.task() is the replacement. Be ready to say where a task handler is registered and that a task must return a value or null.

for a middle

Explain why the shell was the problem: OS, shell, login environment and attached terminal all leaked into the test. Contrast parsing stdout with receiving a structured value.

for a senior

Be ready to describe migrating a real suite off cy.exec() - finding the call sites, deciding which become tasks and which become HTTP calls, and deleting execTimeout from the config.

for a principal

Own the argument for keeping the Node-side surface small: every task is a permanent extension point your team maintains, so justify each one rather than porting shell scripts across wholesale.

## What was removed, and what happens if you call it `cy.exec()` is gone as of Cypress **16.0.0**. It was deprecated in `15.21.0` and removed in the next major. Calling it in a spec no longer runs anything: the driver throws immediately with *"`cy.exec()` was removed in Cypress version 16.0.0. Please update to use `cy.task()` instead, which runs in Node and does not depend on the operating system, shell, or terminal of the machine running Cypress."* The `execTimeout` configuration option went with it. Cypress prints a warning saying it was removed in 16.0.0 and then ignores the value, so a configuration that still carries `execTimeout: 120000` bounds nothing at all. Delete it and reach for `taskTimeout` instead. This is worth knowing precisely, because a large share of published Cypress material still opens its seeding section with `cy.exec('npm run db:reset')`. On Cypress 16 that line is a hard failure at the moment the command runs, not a deprecation notice you can defer. ## Why the shell door was closed `cy.exec()` ran a command **through the shell on the machine running Cypress**, which made a test's setup step depend on things a test has no business depending on: - the operating system, and therefore which quoting and escaping rules apply to the command string - which shell is configured, and whether its login profile has been sourced - whether a terminal is attached — different on a developer laptop than under a CI runner - `PATH` resolution for the binary named in the string - the output contract: you received `stdout` as text and had to parse a value back out of it `cy.task()` has none of that surface. Its handler runs in the **Node process that loaded your Cypress configuration**, so there is no shell, no per-platform quoting, no `PATH` guessing — and the value the handler returns arrives in the spec as a value, not as text. ## The replacement, end to end 1. Register a handler for the `task` event inside `setupNodeEvents`, keyed by an event name of your choosing (`'seedExpenseReport'`, `'resetExpenseLedger'`). 2. Call `cy.task(event)`, `cy.task(event, arg)` or `cy.task(event, arg, options)` from the spec. 3. Consume the yielded value with `.then()`, `.its()` or a chained assertion. From the spec's side, the only contract is the event name and whatever comes back; the handler itself lives with the rest of your Cypress configuration. | Cypress 15 and earlier | Cypress 16 | |---|---| | `cy.exec('node seed-expenses.js')` | `cy.task('seedExpenseReport', payload)` | | yields `{ stdout, stderr, exitCode }` | yields whatever the handler returned | | `execTimeout` bounds it | `taskTimeout` bounds it, default `60000` ms | | runs in a shell on the host machine | runs in Cypress's own Node process | | output is text the spec must parse | value arrives already structured | ## The contract `cy.task()` imposes - **The handler must return something.** Returning `undefined` fails the command with *"The task 'X' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled."* Return `null` explicitly when there is nothing to hand back. - **An unregistered event name fails too**, and the error lists every task name that *is* registered, which makes a typo obvious rather than mysterious. - **Exactly one argument crosses the boundary.** `cy.task(name, arg, options)` has room for a single `arg`; pass an object and destructure it on the Node side when you need several values. - **The argument is serialized with `JSON.stringify()`.** Functions, regular expressions and symbols are omitted to `null`, and a `Date` arrives on the other side as a string. ## What if the thing you need to run is not Node? Spawn it from inside the handler. Node's `child_process.execFileSync` with an argument array — `execFileSync('docker', ['exec', 'db', 'reset-script'])` — runs the binary directly and skips the shell, which is exactly the property whose absence made `cy.exec()` unreliable. The difference is that the spawning is now explicit, lives in one place instead of scattered through specs, and the task can hand the spec a real value rather than a blob of stdout. ## What this looks like in an expense-report suite A suite that used to shell out to reset the ledger and then scrape a report id out of stdout now calls `cy.task('resetExpenseLedger')` followed by `cy.task('seedExpenseReport', { employee: 'jane', total: 42.5 })`, and the second call yields the created report object. The spec goes straight to `cy.visit('/reports/' + report.id)` with no string parsing, no `.trim()`, and no guessing about which shell CI happened to give it.

  • In Cypress 16, what happens if you leave execTimeout in your configuration file?
    It is ignored. Cypress prints a warning saying `execTimeout` was removed in 16.0.0 and points you at `cy.task()` and `taskTimeout`, then carries on with the option having no effect at all. Delete it, and set `taskTimeout` if the default 60 seconds is not enough for your seeding work.
  • Your setup needs a database CLI that is not a Node module. How, without cy.exec()?
    Spawn it from inside the task handler. Node's `child_process.execFileSync` with an argument array runs the binary directly and avoids the shell, so quoting and `PATH` behave the same on every platform. The handler returns whatever the spec needs - the output, or `null` - and the spec still just calls `cy.task('resetExpenseLedger')`.

saying these in an interview costs you the question

  • Claims cy.exec() still works if you avoid CI
  • Parses stdout from a task instead of returning a structured value
  • Thinks cy.task() runs in the browser beside the test code
  • Keeps execTimeout in the config expecting it to bound tasks
  • Returns nothing from the handler and calls the failure a Cypress bug
open as a page

In Cypress, how does cy.fixture('receipts') resolve a file with no extension?

level: juniorimportance: must knowfreq 76%

basics

~20 s

Cypress joins the path onto fixturesFolder, which defaults to cypress/fixtures. If that exact file exists it is read; otherwise Cypress tries a fixed extension list, .json first, and yields the first match, parsed according to that extension.

open as a page

In Cypress, when should a repeated step be a plain function instead of a custom command?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Make it a custom command only when the behaviour is wanted across all your specs, such as signing in or seeding an expense report. A step that one spec file repeats should stay a plain JavaScript function in that file.

open as a page

In Cypress, what does the id argument to cy.session() do, and when does setup run?

level: juniorimportance: must knowfreq 78%

basics

~20 s

The id is the cache key. Cypress runs the setup function only when no valid session is saved under that id; every later call with the same id skips setup and re-applies the cached cookies, localStorage and sessionStorage.

open as a page

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

level: juniorimportance: must knowfreq 72%

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.

open as a page

Why doesn't a Cypress cy.request() appear in the browser's Network tab?

level: middleimportance: must knowfreq 70%

basics

~20 s

Cypress makes the request from its Node process, not as a browser XHR, so DevTools never shows it. Because the browser never issues it, CORS is bypassed and cy.intercept does not see it; cookies are still attached and applied.

open as a page

In Cypress, what does cy.session()'s validate option do when a restored session fails it?

level: middleimportance: must knowfreq 66%

basics

~20 s

validate re-checks a session after setup creates it and after a cached one is restored. When a restored session fails, Cypress recreates it by re-running setup and validates again; a failure straight after setup fails the test.

open as a page

In Cypress, why does a cached cy.session() login start failing in later tests?

level: seniorimportance: must knowfreq 66%

basics

~20 s

The session was cached with no validate function, so Cypress restores the saved cookies and storage without checking them. Once the credential dies mid-run, setup is never re-run and every test after the first lands on the sign-in page.

open as a page

In a Cypress test, how do you read fixture data aliased with .as('receipts')?

level: middleimportance: should knowfreq 58%

basics

~20 s

Two ways: this.receipts inside a test written with function(), because Cypress stores non-DOM aliases on Mocha's shared test context, or cy.get('@receipts') followed by .then(). Either only works after the queued .as() command has actually run.

open as a page

In Cypress, which files can cy.readFile() reach that cy.fixture() cannot?

level: middleimportance: should knowfreq 46%

basics

~20 s

Anything under the project root. cy.readFile() resolves its path from the folder holding the Cypress config, so it reads a downloaded reimbursement CSV or a generated report, while cy.fixture() is confined to fixturesFolder, by default cypress/fixtures.

open as a page

In Cypress, what does the Command Log show when a custom command runs?

level: middleimportance: should knowfreq 46%

basics

~20 s

It shows the commands the body ran, not the command you called. Cypress writes no Command Log entry for a custom command itself, so cy.approveReport() appears as a bare run of get, click and request rows with nothing naming it.

open as a page

Why is the page blank after cy.session() in a Cypress end-to-end test?

level: middleimportance: should knowfreq 52%

basics

~20 s

cy.session() inherits Cypress's testIsolation setting, and with test isolation enabled it clears the page to about:blank both before setup runs and again before the command ends. Whatever setup visited is gone, so call cy.visit() yourself afterwards.

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

A Cypress cy.task() seeding a database times out after 60 seconds - what do you do?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Sixty seconds is the taskTimeout default. Diagnose first: a task that never ends, or waits on a dependency, will not be fixed by a bigger number. If the work is genuinely heavy, raise the narrowest scope, ideally the per-call timeout option.

open as a page

In a Cypress run, how many times does cy.fixture() actually read a file from disk?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Once per path and encoding. Cypress caches the parsed contents in the browser-side driver and serves every later call from that cache, handing back a fresh clone each time so one test cannot corrupt another's copy.

open as a page

Which browser state does Cypress's cy.session() cache, and which state persists anyway?

level: seniorimportance: should knowfreq 38%

basics

~20 s

cy.session() caches and restores cookies, localStorage and sessionStorage across all domains, and nothing else. IndexedDB and every other storage mechanism is neither saved with a session nor cleared when one is restored; it survives the whole browser run.

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

When should a Cypress suite seed through cy.request() rather than cy.task()?

level: principalimportance: should knowfreq 38%

basics

~20 s

Default to cy.request when an endpoint can express the state, because the record goes through the app's own validation and the session travels with it. Reserve cy.task for states no endpoint exposes and for work that is not HTTP.

open as a page

In Cypress, when is a Cypress.Commands.overwrite() in the support file worth it?

level: principalimportance: should knowfreq 38%

basics

~20 s

Only for a rule that must hold everywhere and that no author should have to remember. The support file loads before every spec, so an overwrite silently redefines a built-in suite-wide; a new named command is usually more honest.

open as a page

Why does a Cypress cy.request() to a redirecting URL yield 200 instead of 302?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Because followRedirect defaults to true, so Cypress follows the chain and yields the last response. Set followRedirect: false to stop at the first one; then status is 302 and redirectedToUrl holds the absolute Location target.

open as a page

In Cypress, what happens when two files add a custom command with the same name?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Nothing is reported. Cypress.Commands.add() only rejects a name belonging to a built-in command or reserved internally by Cypress; a second registration of one of your own names silently replaces the first, and whichever file loads last wins.

open as a page

In Cypress, why is cy.fixture() a poor way to load a 40 MB receipt archive?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Because it reads the whole file into memory in the Cypress Node process, ships it to the browser as a single internal WebSocket message, and keeps it cached. Large archives can exhaust memory or exceed socket buffer limits.

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

In a Cypress suite, when is cy.session()'s cacheAcrossSpecs option worth turning on?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Turn it on when setup is slow and many specs on one machine share an identity. The cross-spec cache is in memory per machine per run, so sharding erodes the saving and every call site must pass identical arguments.

open as a page