skip to content

Which Cypress commands let a spec running in the browser reach the file system?

level: juniorimportance: must knowfreq 72%

answer

  1. Spec code is ordinary page JavaScript
  2. No fs module exists in the browser
  3. Four commands cross to the Node side
  4. Fixtures, read, write, and the escape hatch
  5. cy.task runs your registered Node handler

basics

~10 s

Only four: cy.fixture(), cy.readFile(), cy.writeFile() and cy.task(). Spec code is browser JavaScript with no fs module, so each of those hands the work to the Node process that loaded the Cypress config.

solid answer

~40 s

A Cypress spec is bundled into a browser page, so it has `window` and `document` but no `fs`, no `child_process` and no database driver. Four commands cross the seam to the Node process that loaded your Cypress config: `cy.fixture()` reads a file under `fixturesFolder`, `cy.readFile()` reads any path under the project root and retries as a query, `cy.writeFile()` writes under the project root, and `cy.task()` runs a handler you registered in `setupNodeEvents` and can therefore do anything Node can do. `cy.request()` is a related door for HTTP, issued from that same Node process rather than from the page. Anything else — spawning a process, opening a connection, listing a bucket — has to be wrapped in a task. As of Cypress 16 there is no `cy.exec()`; it was removed.

code

javascript · 18 lines
javascript
// cypress/e2e/tenant-settings.cy.js
it('shows the staging flags for a provisioned tenant', () => {
  // read: resolved from the project root, retries until the assertion passes
  cy.readFile('config/environments/staging.json')
    .its('featureFlags.auditLog')
    .should('eq', true)

  // task: anything Node can do that no other command covers
  cy.task('provisionTenant', { slug: 'acme', environment: 'staging' })
    .its('tenantId')
    .then((tenantId) => {
      cy.visit(`/tenants/${tenantId}/settings`)
      cy.contains('[data-cy=env-badge]', 'staging').should('be.visible')
    })

  // write: on the machine running Cypress, under the project root
  cy.writeFile('cypress/reports/provisioned.json', { slug: 'acme' })
})

go deeper

for a junior

Be ready to name the commands that cross into Node — cy.fixture(), cy.readFile(), cy.writeFile() and cy.task() — and to say why a spec has no fs module of its own.

for a middle

Explain the seam itself: the spec is page JavaScript talking over Cypress's own channel to the Node process that loaded the config, and every value crossing it is serialised.

for a senior

Show how you choose which door a piece of setup goes through, and how you keep the Node side small enough that a failure there still reads clearly in the run output.

for a principal

Own the rule for what a suite may reach through these doors at all, and what that reach costs once the Node side grows into a second codebase nobody tests.

## Why a spec cannot open a file A Cypress spec is not a Node script. It is bundled and loaded into a browser page, in the same event loop as the application under test, so the globals it gets are the browser's globals: `window`, `document`, `fetch`, `localStorage`. There is no `fs`, no `child_process`, no `net`, no database driver. Nothing running in that page can open a file handle, spawn a process or hold a raw TCP socket, and that is a property of the browser rather than a Cypress setting you can turn off. The other half of Cypress is an ordinary Node process — the one that read `cypress.config.js` and called `setupNodeEvents`. That process has the whole standard library available. Cypress connects the two halves over its own channel, and a small, fixed set of commands is how a spec sends work across it. ## The four doors | command | what it reaches | yields | retries? | |---|---|---|---| | `cy.fixture()` | a file under `fixturesFolder` | the parsed contents | no | | `cy.readFile()` | any path under the project root | the parsed contents | yes, it is a query | | `cy.writeFile()` | any path under the project root | `null` | no | | `cy.task()` | anything Node can do | whatever the handler returns | no | `cy.request()` is a fifth door of a different kind. It makes an HTTP request from the Node process rather than from the page, which is why the browser's same-origin rules do not apply to it and why it never appears in the browser's own network activity. It reaches services, not files. ## cy.task() is the general-purpose door Everything the first three commands do not cover goes through `cy.task()`, and the shape is always the same: 1. Register a named handler under the `task` event inside `setupNodeEvents`, in the Cypress config file. 2. Call it from the spec as `cy.task('name', arg)`, optionally with a `timeout` option. 3. Have the handler return a value; a handler that returns nothing fails the command, so return `null` when there is nothing to hand back. For a multi-tenant admin console that means the spec never talks to the database directly. It calls `cy.task('provisionTenant', { slug: 'acme', environment: 'staging' })`, and a handler in Node runs the insert with a real client and returns the created row as plain data the spec can assert on. ## What is still out of reach The doors move the work; they do not move where the code runs, so several limits survive them: - **Live handles cannot cross.** Everything a task returns is serialised, so a database client, an open stream or a server object arrives in the spec without its methods. - **A task has to end.** Starting a web server, watching for file changes, or any process you would normally interrupt by hand is not a task; the command fails once `taskTimeout` — 60 seconds by default — elapses. - **There is no browser-side file system.** `cy.writeFile()` writes on the machine running Cypress, under the project root, which is the directory holding the Cypress config file. The page never touches disk. - **Nothing on the Node side runs per test on its own.** There is no `setupNodeEvents` event for each test; if you need Node code before every test, call a task from a global Mocha `beforeEach` hook in the support file. - **The seam is one argument wide.** `cy.task(name, arg, options)` carries a single argument, so multiple values are packed into one object and destructured in Node. ## Cypress 16: cy.exec() is gone A great deal of older material reaches for `cy.exec()` to shell out — seeding a database with a CLI, listing a downloads directory, resetting a container. As of Cypress 16 that command has been removed. Calling it throws, and the `execTimeout` option that went with it prints a warning and is otherwise ignored, so it can simply be deleted from a config. The replacement is a task. Because a task runs inside the Node process, there is no shell to resolve, no per-platform quoting rules, and no dependence on whether a terminal is attached — the three things that made `cy.exec()` behave differently on a developer laptop and a CI container. When the thing you need genuinely is an external binary, spawn it from inside the handler with Node's `child_process.execFileSync()` and an argument array, which avoids the shell entirely, and return a parsed result rather than raw stdout the spec has to pick apart. That is the whole map. If a piece of setup fits none of the four commands, it does not belong in the spec: it belongs behind a task, or outside Cypress altogether.

  • Where do Cypress's cy.readFile() and cy.writeFile() resolve a relative path from?
    From the project root — the directory that contains the Cypress configuration file — not the spec's folder and not the shell's working directory. `cypress run --project <path>` moves that root, which is the usual reason a path that works locally misses in a monorepo pipeline.
  • Does a cy.task() handler run in the same Node process as setupNodeEvents?
    Yes. Handlers registered under the `task` event run in the process that loaded the Cypress config, and that process lives for the whole run. Module-scope state there — a database client, a counter, a cached token — survives between task calls, which is how long-lived resources are kept out of the payload.

saying these in an interview costs you the question

  • Says a Cypress spec can just import Node's fs module
  • Thinks cy.readFile() reads from the browser's own storage
  • Reaches for cy.exec() to shell out, removed in Cypress 16
  • Believes a cy.task() handler runs inside the browser page
  • Cannot name one command that crosses into the Node process