How Cypress Handles File Downloads in Headless and Headed Modes

Cypress handles file downloads by intercepting native browser events—via the will‑download listener in Electron, the Chrome DevTools Protocol Page.setDownloadBehavior in Chromium, and WebDriver preferences in Firefox—and routing files to a configurable downloadsFolder, while emitting standardized automation events (create:download, complete:download, canceled:download) that behave identically in both headed and headless modes.

The cypress-io/cypress repository implements a browser‑agnostic download management layer that abstracts platform‑specific mechanisms into a unified automation API. Whether you run tests with a visible browser window or in a headless CI environment, Cypress configures the browser to write downloads directly to the project’s downloadsFolder and monitors completion through native events, eliminating the need for manual UI interaction.

Browser‑Specific Download Implementations

Electron: The will‑download Event

In packages/server/lib/browsers/electron.ts, Cypress attaches a listener to the Electron session object’s will‑download event. When a download initiates, the handler constructs the absolute destination path using path.join(dir, downloadItem.getFilename()) and emits a create:download event to the automation layer. Completion is tracked via downloadItem.once('done'), which fires either complete:download or canceled:download depending on the outcome.

To ensure downloads proceed without user confirmation, Cypress also sends the CDP command Page.setDownloadBehavior with {behavior: 'allow', downloadPath: dir}.

Source: electron.ts lines 99‑108

Chrome and Chromium: CDP Protocol

For Chromium‑based browsers, packages/server/lib/browsers/chrome.ts establishes a Chrome DevTools Protocol (CDP) session and issues Page.setDownloadBehavior with the downloadsFolder path. This command is essential for headless mode, where no download UI exists, allowing the browser to write files directly to disk. The implementation monitors CDP events to translate browser‑native download notifications into the standard Cypress automation events.

Source: chrome.ts lines 260‑332

Firefox: WebDriver Preferences

In packages/server/lib/browsers/firefox.ts, Cypress configures Firefox through the WebDriver protocol by setting the preference browser.download.dir to the resolved downloadsFolder and browser.download.folderList to 2 (use custom folder). This forces Firefox to bypass its default download manager and write files directly to the Cypress‑controlled directory, regardless of headed or headless state.

Source: firefox.ts lines 475‑485

WebKit: Native Download Events

The WebKit implementation in packages/server/lib/browsers/webkit.ts listens for the browser’s native download event, mapping it to the same create:download and complete:download event structure used by Electron and Chrome.

Source: webkit.ts lines 44‑55

Headless vs Headed Mode Differences

The primary distinction between modes is UI visibility, not download handling:

  • Headed mode: The browser window is visible (show: true). Electron windows render at the configured width/height, and Chrome shows the native download bar. Cypress still intercepts events via the automation layer defined in packages/driver/src/cypress/downloads.ts.
  • Headless mode: No window is displayed (show: false). Chrome relies entirely on the CDP Page.setDownloadBehavior command because the download UI is unavailable. Electron continues to use the same will‑download listener, simply without rendering the window.

Both modes write files to the same downloadsFolder and emit identical automation events, ensuring test portability across environments.

Configuration and Automation Events

The download system centers on three automation events defined in the driver layer:

  • create:download: Fires when a download starts
  • complete:download: Fires when a download finishes successfully
  • canceled:download: Fires when a download is aborted

Configure the destination folder in your Cypress configuration file:

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    downloadsFolder: 'cypress/downloads',
    // Downloads work identically in headed and headless modes
  }
})

Practical Code Examples

Basic Download Verification

// cypress/e2e/downloads.cy.ts
describe('File Downloads', () => {
  it('downloads and verifies a CSV file', () => {
    cy.visit('/data-export')
    cy.get('[data-cy=download-csv]').click()
    
    // Cypress waits for the file to appear in downloadsFolder
    cy.readFile(`${Cypress.config('downloadsFolder')}/report.csv`)
      .should('contain', 'Date,Revenue,Expenses')
  })
})

Handling Binary Files

// Read a downloaded PDF as binary
cy.readFile(`${Cypress.config('downloadsFolder')}/invoice.pdf`, null)
  .then((file) => {
    expect(file.length).to.be.greaterThan(1024)
    expect(file).to.be.an.instanceof(Buffer)
  })

Advanced: Listening to Download Events

// cypress/support/e2e.ts
Cypress.on('task', {
  onDownloadCreated({ id, filePath, mime }) {
    cy.log(`Download started: ${filePath} (${mime})`)
    return null
  }
})

Summary

  • Cypress intercepts downloads at the browser level using Electron’s will‑download event, Chrome’s CDP Page.setDownloadBehavior, or Firefox’s WebDriver preferences.
  • File paths resolve to Cypress.config('downloadsFolder'), defaulting to cypress/downloads relative to the project root.
  • Automation events (create:download, complete:download, canceled:download) provide a cross‑browser API that functions identically in headed and headless modes.
  • Headless Chrome requires CDP configuration to enable downloads since the native UI is unavailable, while headed Electron shows the download bar, but both use the same underlying event system.

Frequently Asked Questions

How does Cypress handle file downloads differently in headless Chrome versus headed Chrome?

In headed Chrome, the browser displays the native download UI while Cypress monitors the download via CDP. In headless Chrome, no UI exists, so Cypress relies exclusively on the Page.setDownloadBehavior CDP command to allow writes to the downloadsFolder. Both modes emit the same complete:download automation events when the file is written.

Can I change the download folder location in Cypress?

Yes. Set the downloadsFolder property in your cypress.config.js file. Cypress resolves this path relative to the project root and passes it to each browser implementation—Electron, Chrome, Firefox, and WebKit—ensuring all downloads route to your specified directory regardless of mode.

Why does my download test work locally (headed) but fail in CI (headless)?

This typically occurs when tests rely on UI interactions that don't exist in headless mode, or when the download trigger requires specific headers. Ensure you use cy.readFile() to assert against the downloadsFolder rather than checking UI elements, and verify that your application triggers the download via a standard anchor tag or API call that Cypress can intercept in both modes.

Does Cypress support download progress monitoring?

Yes. The automation layer emits create:download when the download starts and complete:download when it finishes. While the public API primarily exposes file existence via cy.readFile(), advanced users can tap into the underlying automation events through Cypress event listeners to track download progress or filenames dynamically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →