Q29 of 48 · Cypress

How do you handle file downloads in Cypress?

CypressMidcypressdownloadsfilesmid

Short answer

Short answer: Cypress doesn't expose a download API. The pragmatic patterns: trigger the download and assert on the file in `cypress/downloads/` via `cy.readFile`, intercept the download URL and assert on the headers/body, or use the underlying `cy.request` to fetch the file directly and validate it.

Detail

Browser-driven downloads in Cypress land in cypress/downloads/ (configured via downloadsFolder). The simplest pattern asserts the file exists and inspects its contents:

cy.get('[data-test=export-csv]').click();
cy.readFile('cypress/downloads/orders.csv', { timeout: 10000 })
  .should('include', 'order_id,customer,total');

Two gotchas:

  • The folder accumulates between runs. Clear it in beforeEach with cy.task (Cypress doesn't auto-clear by default).
  • The browser may not actually trigger a real download in headless mode; it depends on the trigger (<a download> works, window.open may not).

For deterministic tests, bypass the click and fetch directly:

cy.request({ url: '/api/exports/orders.csv', encoding: 'binary' })
  .its('body')
  .should('include', 'order_id');

This is faster, more reliable, and tests the actual content. Use the click flow only when you specifically want to verify the trigger UI works.

A third option is to intercept the download request and assert on it without writing to disk:

cy.intercept('GET', '/api/exports/orders.csv').as('export');
cy.get('[data-test=export-csv]').click();
cy.wait('@export').its('response.statusCode').should('eq', 200);

Each pattern tests a different layer; pick the one closest to the contract you actually care about.

// EXAMPLE

download.cy.ts

// Pattern 1: assert on the saved file
it('downloads the orders CSV', () => {
  cy.task('clearDownloads');                  // custom task, see config
  cy.visit('/orders');
  cy.get('[data-test=export-csv]').click();
  cy.readFile('cypress/downloads/orders.csv', { timeout: 10000 })
    .should('include', 'order_id,customer,total');
});

// Pattern 2: bypass the UI, fetch the file directly
it('exports CSV with the right rows', () => {
  cy.request({ url: '/api/exports/orders.csv', encoding: 'binary' })
    .then((res) => {
      expect(res.headers['content-type']).to.include('text/csv');
      const lines = (res.body as string).split('\n');
      expect(lines).to.have.length(101); // header + 100 rows
    });
});

// Pattern 3: assert on the network call only
it('triggers the download endpoint', () => {
  cy.intercept('GET', '/api/exports/orders.csv').as('export');
  cy.visit('/orders');
  cy.get('[data-test=export-csv]').click();
  cy.wait('@export').its('response.statusCode').should('eq', 200);
});

// WHAT INTERVIEWERS LOOK FOR

Knowing all three patterns and when to choose each: file assertion for end-to-end, `cy.request` for deterministic content checks, intercept for trigger-only verification.

// COMMON PITFALL

Forgetting to clear `cypress/downloads/` between runs and getting cross-test pollution. Or relying solely on `cy.readFile` when `cy.request` would be faster and more reliable.