Resource library

QA How-To

How to Download a file in Cypress (2026)

Learn cypress how to download a file, wait for it safely, verify CSV and binary content, handle dynamic names, and keep download tests reliable in CI.

24 min read | 2,975 words

TL;DR

Click the real download control, then use `cy.readFile('cypress/downloads/expected-name.csv')` and assert the file's business content. `cy.readFile()` retries until the file exists and chained assertions pass. For a direct endpoint test, use `cy.request({ encoding: 'binary' })` and write or inspect the response deliberately.

Key Takeaways

  • Trigger at least one download through the real UI when the browser workflow is the risk.
  • Read known filenames with retryable `cy.readFile()` from the configured downloads folder.
  • Assert file meaning, such as rows, headers, identifiers, and format signatures, not existence alone.
  • Use `cy.request()` with binary encoding when the test is specifically about the file endpoint rather than the browser click.
  • Treat dynamic filenames, temporary extensions, duplicate suffixes, and parallel workers as explicit design concerns.
  • Keep generated downloads out of source control and preserve useful artifacts only when failure investigation needs them.
  • Test authorization, dangerous spreadsheet formulas, and sensitive export data as part of the download contract.

The direct answer to cypress how to download a file is: trigger the download, wait on a deterministic signal, then verify the saved artifact under Cypress's configured downloadsFolder. With the default configuration, browser downloads go to cypress/downloads, and cy.readFile() can retry until a known file exists and its chained assertions pass.

A useful download test goes beyond checking that a button was clickable. It proves the request was authorized, the browser received a file, the filename and media contract are sensible, and the content represents the selected data. The exact depth depends on risk, so this guide separates UI download coverage, endpoint coverage, text parsing, binary validation, dynamic names, and CI concerns.

TL;DR

Scenario Recommended approach Primary assertion
Stable CSV filename from a UI click Click, then cy.readFile() Parsed headers and business rows
JSON export Click, then cy.readFile() Parsed object schema and values
PDF or ZIP from a stable filename Read with null encoding Magic bytes, nonzero size, then deeper parser if needed
Download endpoint only cy.request() with binary encoding Status, headers, and payload
Dynamic server filename Observe response metadata plus a polling cy.task() One completed matching file
CI download Unique expected name per worker Correct content in the worker's project path

Existence is only the first checkpoint. The strongest assertion is usually a domain fact, such as the exported invoice ID, requested date range, exact column set, or expected record count from controlled test data.

1. Cypress How to Download a File: Know the Browser Pipeline

A download can involve several independent contracts. The UI constructs parameters, the server authorizes the request, the response supplies bytes and headers, the browser chooses a filename and writes to disk, and the consumer opens the artifact. A single Cypress end-to-end test should not necessarily validate every byte transformation, but it should identify which boundaries matter.

Cypress saves files downloaded by the application into downloadsFolder, which defaults to cypress/downloads. In run mode, trashAssetsBeforeRuns defaults to true, so generated downloads, screenshots, and videos are cleared before a run. Add the generated folder to .gitignore rather than committing test artifacts:

cypress/downloads/

Cypress does not provide a cy.download() command. You normally click the application's link or button. If the product exposes a file URL and the browser flow is not under test, cy.request() can exercise the endpoint directly. Do not invent a command or install a plugin before understanding the built-in paths.

Separate three questions in review:

  • Did the user action request the correct export?
  • Did the service return the correct file contract?
  • Did the downloaded content contain the correct data?

That separation improves failures. A missing file can mean no click, a 403, a popup or new-origin flow, a browser write still in progress, or an unexpected filename. File existence alone cannot explain which boundary broke.

2. Configure the Downloads Folder and Stable Test Data

The default path is usually the simplest. If your repository needs another location, configure it explicitly and make test paths agree:

import { defineConfig } from 'cypress'

export default defineConfig({
  downloadsFolder: 'cypress/downloads',
  trashAssetsBeforeRuns: true,
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
})

Paths passed to cy.readFile() are relative to the Cypress project root, the directory containing the Cypress configuration file. Do not build them from the spec file location. Keep a small helper constant if many specs share the same configured folder, but avoid hiding filenames and expectations inside a generic download utility.

Stable data is more important than a stable delay. Seed a report with a unique ID, fixed rows, a known timezone, and an account authorized to export it. If the product names files from the report ID, the expected filename becomes deterministic. If it uses timestamps, either control the browser date when the name is client-generated or read trusted response metadata when the server chooses the name.

Avoid reusing report.csv across parallel tests. One worker can read another worker's file or overwrite it. Prefer a name such as invoice-export-inv_20260713_a.csv, where the unique portion comes from test setup. If the product cannot accept a unique name, isolate project directories per worker or design a task that selects a file created after the current test started.

Never place production customer exports in fixtures or CI artifacts. Generated test data should be synthetic and safe even if a failure retains the file.

3. Trigger a UI Download and Verify a Known Filename

For the core user journey, click the real control. Register an intercept before the click if the application uses a request that Cypress can observe. Then read the expected file.

it('downloads the selected invoice export', () => {
  const filename = 'invoice-export-inv_20260713_a.csv'

  cy.intercept('GET', '/api/invoices/inv_20260713_a/export*')
    .as('exportInvoice')

  cy.visit('/invoices/inv_20260713_a')
  cy.get('[data-cy=download-invoice]').click()

  cy.wait('@exportInvoice').then(({ response }) => {
    expect(response?.statusCode).to.eq(200)
    expect(response?.headers).to.have.property('content-type')
  })

  cy.readFile(`cypress/downloads/${filename}`, 'utf8', {
    timeout: 15000,
  }).should('include', 'inv_20260713_a')
})

The intercept confirms the intended operation. The read confirms a saved file. The content assertion confirms this is not merely an old file with the same name. cy.readFile() is a query in current Cypress versions, so it retries when the file does not exist yet and rereads it while chained assertions fail. A local timeout can reflect the accepted export time without slowing a fast download.

Do not put cy.readFile() inside a manual loop. Do not add a fixed sleep before it. If the filename is known, the built-in retry behavior is the direct synchronization mechanism.

Adapt the route, ID, filename, and timeout to your application contract. If the export response redirects through a different service or opens a top-level cross-origin page, the intercept may not observe the whole browser path. The saved file remains a user-facing result, but you will need evidence from the actual flow rather than assuming this exact route shape.

4. Parse and Assert CSV Content Instead of Raw Substrings

A substring can prove an identifier exists, but CSV has quoting, commas inside values, embedded line breaks, byte-order marks, and column-order rules. For production-grade CSV tests, use a maintained CSV parser already approved in the project. The example below uses papaparse, which must be installed as a development dependency:

npm install --save-dev papaparse @types/papaparse
import Papa from 'papaparse'

type InvoiceRow = {
  invoice_id: string
  customer: string
  total: string
  status: string
}

it('exports one approved invoice row', () => {
  cy.visit('/reports/invoices')
  cy.get('[data-cy=download-csv]').click()

  cy.readFile('cypress/downloads/invoices-2026-07-13.csv', 'utf8', {
    timeout: 15000,
  }).then((csvText) => {
    const result = Papa.parse<InvoiceRow>(csvText, {
      header: true,
      skipEmptyLines: true,
    })

    expect(result.errors, 'CSV parse errors').to.deep.eq([])
    expect(result.meta.fields).to.deep.eq([
      'invoice_id',
      'customer',
      'total',
      'status',
    ])
    expect(result.data).to.deep.eq([
      {
        invoice_id: 'inv_20260713_a',
        customer: 'Northwind QA',
        total: '125.50',
        status: 'approved',
      },
    ])
  })
})

This test asserts structure and meaning. It controls the source record, so exact equality is appropriate. For a larger export, assert the expected controlled records and invariants without hard-coding unrelated ordering unless order is part of the requirement.

Spreadsheet safety also matters. If user-provided cells can begin with =, +, -, or @, verify the product applies its documented formula-injection protection. Do not silently sanitize inside the test parser, since that would hide the exported bytes the spreadsheet consumer receives.

5. Verify JSON, Text, and Header Contracts

cy.readFile() automatically parses a .json file into JavaScript when no text encoding forces a different treatment. That makes a JSON export concise to verify:

it('downloads a JSON audit export', () => {
  cy.visit('/audit')
  cy.get('[data-cy=export-json]').click()

  cy.readFile('cypress/downloads/audit-export.json', {
    timeout: 15000,
  }).should((document) => {
    expect(document).to.have.property('schemaVersion', 2)
    expect(document.generatedFor).to.eq('org_test_42')
    expect(document.entries).to.deep.include({
      action: 'invoice.approved',
      actor: 'qa-user@example.test',
    })
  })
})

For plain text, specify utf8 and assert newline and encoding rules only when they are contractual. Normalize line endings if the product intentionally supports both LF and CRLF; do not normalize if a downstream importer requires one.

File response headers deserve an endpoint or intercept assertion. Content-Type identifies the media type, while Content-Disposition often provides attachment and a filename. Header values can vary in case and may use filename* for international names, so avoid a brittle whole-string comparison unless the server contract mandates it.

The file extension is not proof of the format. A server can return an HTML error page named .csv, or JSON with a .pdf name. Assert response status and content type when observable, then parse or inspect the saved bytes. The combination catches failures that a filename-only check misses.

6. Validate PDF, ZIP, and Other Binary Downloads

Pass null as the encoding to receive a Cypress.Buffer. You can check a known file signature and minimum meaningful size without corrupting the bytes through UTF-8 conversion.

it('downloads a PDF invoice', () => {
  cy.visit('/invoices/inv_20260713_a')
  cy.get('[data-cy=download-pdf]').click()

  cy.readFile(
    'cypress/downloads/invoice-inv_20260713_a.pdf',
    null,
    { timeout: 15000 },
  ).should((pdf) => {
    expect(Cypress.Buffer.isBuffer(pdf)).to.eq(true)
    expect(pdf.subarray(0, 5).toString('ascii')).to.eq('%PDF-')
    expect(pdf.length).to.be.greaterThan(100)
  })
})

The 100 byte threshold is illustrative, not a quality benchmark. Choose a lower bound only to reject an obviously empty or truncated artifact, and do not claim it validates the PDF structure. For a business-critical PDF, parse it in a Node task with a maintained library, assert page count or extracted invoice facts, and return serializable results to the spec. For ZIP, validate the PK signature, then use a ZIP parser to inspect entries and CRC errors.

Keep binary work out of browser DOM logic. A task is suitable when a parser depends on Node APIs, but its purpose should be explicit. Do not call an external desktop application from CI just to prove a file opens. Format parsers and consumer contract tests are more deterministic.

If the export is large, avoid logging the whole buffer or attaching sensitive bytes to assertion messages. Assert small metadata and selected content. Preserve the artifact only when policy and failure investigation justify it.

7. Handle Dynamic Filenames and Partial Files Safely

Server-generated names can include a timestamp, localized title, or unique job ID. Prefer an observable contract before scanning a directory. If Content-Disposition supplies the name, capture and parse that header in the request layer, then use it as the expected disk name. Parsing must support the formats your server actually emits.

When the browser controls the final name and it is not exposed reliably, a Node task can poll for exactly one completed match. Temporary extensions such as .crdownload or .part must be excluded. The following task returns a serializable path and times out clearly:

import { defineConfig } from 'cypress'
import fs from 'node:fs/promises'
import path from 'node:path'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        async waitForDownload({ pattern, startedAt }) {
          const folder = path.resolve(config.projectRoot, config.downloadsFolder)
          const deadline = Date.now() + 15000
          const regex = new RegExp(pattern)

          while (Date.now() < deadline) {
            const names = await fs.readdir(folder).catch(() => [])
            const matches = []

            for (const name of names) {
              if (!regex.test(name) || /\.(crdownload|part)$/i.test(name)) continue
              const filePath = path.join(folder, name)
              const stat = await fs.stat(filePath)
              if (stat.mtimeMs >= startedAt && stat.size > 0) matches.push(filePath)
            }

            if (matches.length === 1) return matches[0]
            if (matches.length > 1) throw new Error(`Multiple downloads matched: ${matches}`)
            await new Promise((resolve) => setTimeout(resolve, 100))
          }

          throw new Error(`No completed download matched ${pattern}`)
        },
      })
    },
  },
})
it('downloads a dynamically named report', () => {
  const startedAt = Date.now()
  cy.visit('/reports/monthly')
  cy.get('[data-cy=download-report]').click()

  cy.task('waitForDownload', {
    pattern: '^monthly-report-2026-07-[0-9]{2}\.csv
#39;, startedAt, }).then((filePath) => { cy.readFile(filePath as string, 'utf8').should('include', 'org_test_42') }) })

Use a narrowly constructed pattern, never untrusted regex input.

8. Test the Download Endpoint Directly With cy.request()

Sometimes the risk is the endpoint contract, not the browser download manager. cy.request() can retrieve binary content, bypass CORS, and run outside the browser. It also bypasses cy.intercept() routes, so do not expect an intercept stub to affect it.

it('returns an authorized PDF export', () => {
  cy.request({
    method: 'GET',
    url: '/api/invoices/inv_20260713_a/pdf',
    encoding: 'binary',
  }).then((response) => {
    expect(response.status).to.eq(200)
    expect(response.headers['content-type']).to.include('application/pdf')
    expect(response.headers['content-disposition']).to.include('attachment')
    expect(response.body.slice(0, 5)).to.eq('%PDF-')

    cy.writeFile(
      'cypress/downloads/direct-invoice.pdf',
      response.body,
      'binary',
    )
  })
})

Writing the response is optional. If the endpoint assertions cover the requirement, avoid producing a file. If downstream parsing operates on a path, writing it can make that test stage explicit.

This is not a substitute for every UI download test. A direct request does not prove the button builds the correct URL, uses current filters, handles loading state, or initiates a browser download. Keep one focused browser journey for that wiring and move broader data combinations to API or service tests. The Cypress cy.request API examples show additional request patterns.

If authentication relies on browser cookies, cy.request() uses matching cookies from Cypress's cookie jar. Still verify that the user role is the one the scenario claims. A convenient authenticated response is not proof of access control.

9. Assert Negative, Authorization, and Security Cases

Download functionality often exposes sensitive bulk data. Add focused endpoint tests for an unauthenticated user, a user from another tenant, a revoked export, and an identifier the caller does not own. Set failOnStatusCode: false so the test can assert the intended denial:

it('does not export another tenant invoice', () => {
  cy.request({
    method: 'GET',
    url: '/api/invoices/inv_other_tenant/pdf',
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.be.oneOf([403, 404])
    expect(response.headers['content-type']).to.include('application/json')
    expect(response.body).to.have.property('code')
  })
})

Choose the exact expected status from your API contract rather than copying this accepted pair. Some systems intentionally use 404 to reduce identifier disclosure. The key is to assert the documented behavior and confirm no file was returned.

Also test filter integrity. If a user exports the current month's approved invoices, seed one approved in-range row plus excluded rows: pending, out-of-range, deleted, and another tenant. Assert only the allowed row appears. This validates authorization and business selection together without needing a huge fixture.

For CSV, cover formula injection and encoding of delimiters, quotes, and newlines. For filenames, test user-provided titles against path separators and control characters at the service layer. For very large exports, test size limits and asynchronous job authorization outside the single UI happy path. The API security testing basics provide a broader access-control checklist.

10. Make Download Tests Reliable in CI and Parallel Runs

A CI worker writes into its checked-out Cypress project. Confirm that the downloads folder exists or can be created, the filesystem is writable, and the container has enough disk space. Do not assume a path from your laptop. Use project-relative paths in specs and config.projectRoot plus config.downloadsFolder in Node tasks.

Parallel runs need unique files or isolated workspaces. If two specs download report.csv in one shared directory, neither content assertion proves ownership. Generate unique business records and expected filenames. When the runner already gives each worker a separate checkout or container, document that isolation instead of adding unnecessary locks.

Asset cleanup occurs before the run, not necessarily before every test. A previous test in the same run can leave a file. Include a unique ID in content and name, or delete a known test-owned file through a Node task before the action. Never delete the entire shared downloads directory from one parallel test.

Artifact retention should be intentional. A failed CSV can help diagnosis, but it may contain synthetic customer-like data or secrets. Configure CI to upload only approved paths, only on failure, with short retention. Do not log whole exports.

Cross-browser coverage should reflect product risk. One browser may be enough for service-generated content, while a browser-specific initiation problem may justify a matrix. Keep deep content parsing outside redundant browser combinations so the same expensive validation is not repeated without additional signal.

11. Cypress How to Download a File: Build a Complete Test Strategy

A mature strategy distributes assertions by risk instead of putting everything into one slow browser spec. Use a small UI test to prove the selected filters and browser initiation. Use API tests for status, headers, permissions, and combinations. Use parser or service tests for detailed file structure, escaping, totals, and large datasets.

Test layer Owns Should not pretend to prove
Browser E2E User control, request wiring, saved artifact, one key fact Every export permutation
API integration Authorization, query parameters, response headers and bytes Browser download behavior
Parser or service Rows, formulas, totals, encoding, format validity Deployed routing and identity
Security Tenant isolation, injection, sensitive fields, limits General UI usability

Review failures using the same layers. If no file appears, inspect the click and response first. If the file is HTML, inspect status and authentication. If parsing fails, preserve a safe sample and inspect encoding or truncation. If only CI fails, inspect paths, permissions, collisions, and artifact cleanup.

For a related opposite-direction workflow, compare Cypress file upload examples. Upload tests focus on choosing browser files and server acceptance, while download tests focus on response delivery and filesystem evidence. Both benefit from format-aware assertions and synthetic safe data.

The result should be a small portfolio of tests with precise failure messages, not one monolithic test that clicks, scans the filesystem, parses every row, opens a document renderer, and tests permissions for five roles.

Interview Questions and Answers

Q: Where does Cypress store downloaded files?

Cypress uses the configured downloadsFolder, which defaults to cypress/downloads. Paths in cy.readFile() are relative to the Cypress project root. I keep generated downloads out of source control.

Q: How do you wait for a downloaded file without a fixed sleep?

For a known path, I use cy.readFile() with a focused timeout and a content assertion. It retries while the file is missing and while chained assertions fail. For a dynamic name, I use response metadata or a bounded Node task that excludes partial files and matches a file created by the current test.

Q: How do you verify a CSV download?

I parse it with an established CSV library and assert parse errors, exact required headers, and controlled business rows. I do not split on commas because quoted fields and embedded newlines make that incorrect.

Q: What is the difference between a UI download test and cy.request()?

The UI test proves the control and browser workflow initiate the correct export. cy.request() tests the endpoint directly outside the browser and is better for headers, permissions, and content combinations. I use each for the boundary it can actually prove.

Q: How do you validate a PDF without opening a desktop viewer?

I read it as a buffer, check the PDF signature and meaningful size, then use a maintained parser in a Node task when page or content validation is required. A signature alone detects obvious wrong formats but does not validate document structure.

Q: Why can a download test pass with the wrong file?

A stale artifact or parallel worker may have created the same filename. I use unique data and names, verify identifying content, and constrain dynamic matches to files created after the test began.

Q: What security cases matter for exports?

I cover missing identity, wrong tenant, insufficient role, revoked resources, filter isolation, sensitive columns, spreadsheet formula injection, and safe filenames. The exact cases come from the product's threat and authorization model.

Common Mistakes

  • Inventing a cy.download() command instead of using the UI or cy.request().
  • Adding a fixed wait before reading a known path.
  • Checking only that a file exists and never asserting its business content.
  • Splitting CSV text on commas instead of using a real CSV parser.
  • Reading binary formats as UTF-8 text and corrupting the bytes.
  • Assuming a .pdf extension proves the response is a PDF.
  • Scanning for the first matching filename without excluding stale or partial files.
  • Reusing report.csv across parallel workers.
  • Expecting cy.intercept() to stub a cy.request() call.
  • Hard-coding an absolute path from a developer laptop.
  • Uploading sensitive download artifacts or logging their complete contents.
  • Using one large browser test for UI wiring, permissions, every data combination, and deep format parsing.

Conclusion

For cypress how to download a file, the dependable pattern is to control the source data, trigger the correct boundary, synchronize on the response or filesystem condition, and assert what the artifact means. A known filename plus retryable cy.readFile() is the simplest browser pattern. cy.request() is the focused endpoint pattern, while Node tasks handle truly dynamic filesystem work.

Begin with one high-value export. Give its test record a unique ID, verify the selected UI action, parse the downloaded content, and add one authorization failure. That small design provides more release confidence than a folder existence check and remains understandable when it fails in CI.

Interview Questions and Answers

How would you test a file download in Cypress?

I seed unique data, register an operation-specific intercept when useful, click the real download control, and read the expected path with `cy.readFile()`. I parse or inspect the artifact and assert a business fact, not only existence. I keep endpoint and deep-format combinations in lower layers.

Why is `cy.readFile()` suitable for waiting on a download?

It is a retryable query in current Cypress versions. It retries when the file does not yet exist and rereads while chained assertions fail. I still use a focused timeout that reflects the accepted export latency.

How would you test an unknown server-generated filename?

First I derive it from `Content-Disposition` or another trusted operation result. If the browser's final name is only visible on disk, I poll in a Node task, exclude partial extensions, filter by creation time and a narrow pattern, and require exactly one match.

How do you prevent false positives in parallel download tests?

I create unique source records and expected names per worker, then assert the unique identifier inside the file. If workers share a filesystem, I isolate folders or scope matches to the current test's start time.

What would you assert for a binary download?

I assert the response status and media headers, read bytes without UTF-8 conversion, check the expected format signature, and validate nontrivial size. For a critical format, I use a maintained parser to assert structure and selected domain content.

When would you choose `cy.request()` over clicking the UI?

I choose it when the target is the service contract, permissions, headers, or content combinations. I retain a smaller UI test to prove the browser control sends the right request and initiates the download.

What download security risks would you test?

I test cross-tenant access, role restrictions, revoked resources, excluded records, sensitive columns, safe filenames, and spreadsheet formula injection. I also review artifact retention so synthetic exports do not expose secrets in CI logs or uploads.

Frequently Asked Questions

Where are files downloaded by Cypress saved?

They are saved in the configured `downloadsFolder`, which defaults to `cypress/downloads`. The folder contains generated artifacts and should normally be ignored by Git.

Does Cypress have a `cy.download()` command?

No. Trigger a download through the application's link or button, or call the file endpoint with `cy.request()` when browser behavior is not the subject of the test.

How can Cypress wait until a download finishes?

For a known filename, call `cy.readFile()` with a suitable timeout and chain a content assertion. It retries until the file exists and the assertion passes.

How do I verify a CSV file downloaded in Cypress?

Read it as UTF-8 and parse it with a maintained CSV parser. Assert required headers, parse errors, and controlled business rows instead of splitting raw text on commas.

How do I verify a downloaded PDF in Cypress?

Read the file with `null` encoding to obtain a buffer, then inspect its `%PDF-` signature and size. Use a PDF parser in a Node task when the requirement includes page or text validation.

Can `cy.request()` download a binary file?

Yes. Set `encoding: 'binary'`, inspect the response, and optionally save it with `cy.writeFile(..., 'binary')`. This tests the endpoint and does not prove the browser UI initiated a download.

How should I handle dynamic download filenames?

Prefer the response's trusted filename metadata. If that is unavailable, use a bounded Node task that excludes partial files, limits the filename pattern, and selects only a file created after the current test started.

Related Guides