ScreenshotNeo

BlogHow-to

How to Upload and Download Files with Cypress

Use Cypress .selectFile() for browser uploads and cy.readFile() to verify downloads. This guide covers fixtures, buffers, drag-and-drop, API transfers, configuration, and troubleshooting.

By the ScreenshotNeo team4 October 202611 min read

Use Cypress .selectFile() to upload a file through a browser file input, and use cy.readFile() to check a browser-triggered download saved in Cypress’s downloads folder. For an existing file, pass its project-relative path directly. Use a fixture or buffer for generated or binary data, and choose { action: 'drag-drop' } when the application handles dropped files.

This guide distinguishes browser interactions from direct HTTP transfers: an upload through the UI tests the file picker or drop target, while cy.request() tests the API. Likewise, trigger the UI to test a browser download, or request and write a response directly when the endpoint is what matters.

1. Upload a file through a file input

For a file already in your project, select the input and pass a path relative to the project root:

cy.get('input[type="file"]')
  .selectFile('cypress/fixtures/example.pdf')

The subject must be one file input or a connected label. Cypress recommends disk paths for files because they avoid many encoding-related pitfalls. The path must resolve from the project root; Cypress waits and retries while resolving a path or waiting for an actionable element, then times out if it cannot complete the command.

To verify that the application accepted the upload, assert on an app-visible result that reflects your requirements, such as a filename, completion message, or rendered preview:

cy.get('input[type="file"]')
  .selectFile('cypress/fixtures/example.pdf')

cy.get('[data-cy=upload-status]')
  .should('contain', 'example.pdf')

Replace the selector and expected message with elements your application actually exposes. The file-selection command only populates the input; it does not prove that the server accepted, stored, or processed the file.

Upload a fixture by alias

For a reusable fixture, load it with cy.fixture() and pass its alias. Use null encoding for a binary fixture so Cypress yields a buffer:

cy.fixture('images/avatar.png', null).as('avatar')
cy.get('input[type="file"]').selectFile('@avatar')

A direct fixture path is also convenient when you do not need to load or reuse its contents first:

cy.get('input[type="file"]')
  .selectFile('cypress/fixtures/images/avatar.png')

cy.fixture() caches contents for a path and encoding. If the test or application changes a file during the run, use cy.readFile() instead; it reads the current file and, since Cypress 13, retries as a query with chained assertions.

Upload generated or binary content

Pass a .selectFile() object when content is generated in the test or when you need to control the filename and metadata. Its contents can be a string, a TypedArray or Cypress.Buffer, a path, or an alias.

const csv = Cypress.Buffer.from('name,email\nAda,ada@example.com\n', 'utf8')

cy.get('input[type="file"]').selectFile({
  contents: csv,
  fileName: 'users.csv',
  mimeType: 'text/csv',
  lastModified: new Date('2024-01-01T00:00:00Z').getTime(),
})

Use an explicit filename because the application can inspect it. Cypress infers a MIME type from recognized extensions; set mimeType if the extension is unknown or the application depends on a specific type. The default lastModified is the current time. Keep binary bytes in a buffer or use a disk path; converting arbitrary binary content to a string can corrupt it.

Select multiple files

Pass an array of paths or file objects when the application accepts several files at once. The input must have the multiple property, or Cypress reports an error.

cy.get('input[type="file"][multiple]')
  .selectFile([
    'cypress/fixtures/first.pdf',
    'cypress/fixtures/second.pdf',
  ])

Test drag-and-drop upload

If the application processes drop events, use action: 'drag-drop' and make the drop target the command subject. Use body if the app attaches its handler at page level; drop events bubble to document listeners.

cy.get('[data-cy=drop-zone]')
  .selectFile('cypress/fixtures/example.pdf', { action: 'drag-drop' })
// For an application listening for drops on the page body
cy.get('body')
  .selectFile('cypress/fixtures/example.pdf', { action: 'drag-drop' })

Choose the element that actually receives the app’s drop handling. Selecting the input tests the file-picker path; it does not exercise a separate drop zone.

Hidden file inputs

Some interfaces hide the real input and open it when a visible button is clicked. If Cypress actionability prevents selecting the hidden input, use { force: true } when the application’s visible control is the intended user entry point:

cy.get('[data-cy=choose-file]').click()
cy.get('input[type="file"]').selectFile(
  'cypress/fixtures/example.pdf',
  { force: true },
)

Keep an assertion on the UI result so the test checks that the app handled the chosen file. Cypress cautions against chaining commands that rely on the subject after .selectFile(); begin the next step with a fresh query.

2. Upload through the HTTP API

When the requirement is to test an upload endpoint rather than the browser’s file input, use cy.request() with FormData. Cypress preserves file bytes and supplies the multipart boundary. Leave form unset: that option is for URL-encoded forms, not multipart uploads.

cy.fixture('example.pdf', null).then((file) => {
  const form = new FormData()
  form.append('file', new Blob([file], { type: 'application/pdf' }), 'example.pdf')

  cy.request({
    method: 'POST',
    url: '/api/uploads',
    body: form,
    headers: {
      // Include the app's required authentication or other headers here.
    },
  }).then((response) => {
    expect(response.status).to.equal(201)
    expect(response.body).to.have.property('id')
  })
})

Use the endpoint, multipart field name, authentication, and success assertions your application requires. This request does not test that a user can open the file picker, select a file, or use a drop zone. Keep a separate browser test if those interactions are part of the requirement.

3. Download a file through the browser

When the application initiates a browser download, Cypress saves the file into the configured downloadsFolder rather than showing a native Save As dialog or download shelf. The default is cypress/downloads. Trigger the app action, then read the expected file and assert on content relevant to the feature.

cy.get('[data-cy=export]').click()

cy.readFile('cypress/downloads/report.csv')
  .should('contain', 'Total')

Choose a meaningful assertion for the format and product requirement. For CSV or text, assert on expected content. For JSON, parse and check required fields. For a binary format, read with null encoding to get a buffer and check the relevant bytes or pass it to a format-aware parser.

cy.get('[data-cy=download-json]').click()

cy.readFile('cypress/downloads/report.json').then((text) => {
  const report = JSON.parse(text)
  expect(report).to.have.property('status', 'complete')
})

If a download can take time, make sure the application exposes a completion signal or that the expected file is created before asserting its final contents. cy.readFile() retries chained assertions and rereads the file, which helps with files generated during a test. Use a predictable filename or derive it from the app’s behavior.

Configure and clean the downloads folder

Set downloadsFolder in Cypress configuration if your project keeps downloaded assets elsewhere. For example:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  downloadsFolder: 'cypress/downloads',
})

The default is already cypress/downloads, so configure this only if you need a different location. trashAssetsBeforeRuns defaults to true. Before cypress run, Cypress clears downloads, screenshots, and videos folders, including nested folders. Do not rely on an asset from a previous run still being present.

Downloaded files are generated artifacts. Cypress documentation lists cypress/downloads/ alongside screenshots and videos as folders commonly excluded in .gitignore:

cypress/downloads/

4. Download an endpoint response directly

If the goal is to check a download endpoint’s response rather than the browser interaction, request it and write the returned bytes. Cypress documents this pattern for binary responses such as PDFs.

cy.request({
  url: '/api/reports/monthly.pdf',
  encoding: 'binary',
}).then((response) => {
  expect(response.status).to.equal(200)
  cy.writeFile('cypress/downloads/monthly.pdf', response.body, 'binary')
})

For a response you already have as a buffer, cy.writeFile() can write the buffer with null encoding, avoiding string conversion:

cy.request('/api/data').then((response) => {
  const bytes = Cypress.Buffer.from(response.body)
  cy.writeFile('cypress/downloads/data.bin', bytes, null)
})

Use this approach for endpoint behavior and content. It bypasses the browser’s download UI, so it does not verify that the user can initiate the download or that the browser saves it under the expected filename.

For Node-side work, such as checking file metadata without moving a large file through browser commands, use cy.task(). Cypress documents tasks as a way to run code in Node; its custom-command examples include a download command implemented through a task. Keep the task focused on the file operation and return a serializable result to the test.

5. Choose the right workflow

Need Use What it exercises
Upload an existing project file in the UI .selectFile('path') File input handling and the app’s upload flow after selection
Reuse fixed test data cy.fixture(), then an alias or path Fixture-backed selection; use null encoding for binary fixtures loaded as contents
Generate file contents in the test .selectFile({ contents, fileName, mimeType }) Input handling with controlled content and metadata
Test drag-and-drop .selectFile(path, { action: 'drag-drop' }) The application’s drop event handling
Test a multipart endpoint cy.request() with FormData API upload behavior, without the browser picker
Test a browser download Trigger UI, then cy.readFile() The app action and downloaded file content in downloadsFolder
Test only a response download endpoint cy.request() then cy.writeFile() Endpoint status and response bytes, without browser download behavior

6. Performance, reliability, and cost

  • Keep test files small and specific. Large fixtures take longer to read, transfer, and process. Use the smallest representative file that exercises the validation or processing behavior under test.
  • Prefer paths for existing files. Passing a project-relative path avoids unnecessary encoding and buffer conversion. Use buffers when content is generated or must stay binary.
  • Use stable names and assertions. Predictable download filenames make tests easy to inspect. Assert on the content or structure that matters instead of relying only on the file’s existence.
  • Account for cleanup. Cypress clears asset folders before a run by default. Tests should create or download their own inputs and outputs rather than depend on stale files.
  • Separate UI and API coverage. Direct requests are efficient for endpoint checks, while browser workflows catch problems in user-facing file selection, drop handling, and download initiation.
  • Keep artifacts out of source control when appropriate. Download outputs are generated files and are commonly ignored with the Cypress downloads folder.

Cypress file commands operate on local project data or responses; no external screenshot service is needed for these file assertions. For a separate test that needs a visual record of a page or PDF, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its pricing is 1,000 shots per month free with no card, then $5 for 3,000 on Starter; yearly billing gives two months free, and every feature is on every plan.

7. Troubleshooting

Symptom Likely cause Fix
selectFile() cannot find the file The path is incorrect, not project-root-relative, or the alias has not been created. Check the file location and pass a path such as cypress/fixtures/example.pdf. If using an alias, create it with cy.fixture() before selecting it.
The input is not actionable The input is hidden or covered, or the selector finds the wrong element. Use the actual input[type="file"], or use { force: true } for an intentionally hidden input activated by the UI.
Multiple-file selection fails The input does not have the multiple property. Confirm that the application supports multiple files and that the rendered input has multiple; otherwise select one file at a time if that matches the app.
Drop zone ignores the file The test targets the input or wrong element while the app listens for drop events elsewhere. Use { action: 'drag-drop' } and target the drop zone or body if the handler is attached at page level.
Binary upload is corrupted or rejected Bytes were converted through a text encoding, or the filename/MIME type is unsuitable. Use a disk path or Cypress.Buffer; set an appropriate fileName and, when needed, mimeType.
Download assertion says the file is missing The app has not produced it yet, the name differs, or Cypress and the test use different folders. Check the actual download filename and configured downloadsFolder. Synchronize with the app’s completion behavior and assert through cy.readFile().
A previous download disappears at run start trashAssetsBeforeRuns is true by default. Have each run create the needed file. Do not use an artifact from an earlier run as fixture data.
API upload rejects the request The field name, authentication, content type, or multipart boundary does not match the endpoint contract. Use the expected field name and credentials, pass FormData, and leave Cypress’s form option unset.

For version context, Cypress documents .selectFile() as added in 9.3.0; TypedArray and mimeType support were recorded in 9.4.0. cy.readFile() became a query in Cypress 13. These are API history markers, not a statement of the latest Cypress release. Check the official command references for the behavior available in your installed version.

Or skip the browser setup

For a visual capture of a page involved in a file workflow, ScreenshotNeo takes a screenshot or PDF with one GET request. Its clean capture steps accept cookie banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, and failed loads are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents using Claude, Cursor, or another MCP client. ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000.

Example request for a screenshot of a page in your file flow (replace the URL and API key):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. Sign up for 1,000 free screenshots a month with no card.

8. FAQ

Does Cypress open the operating system’s file chooser?

No. .selectFile() sets the file input in the browser test; it does not automate the native file chooser dialog.

Can I upload a file without storing it as a fixture?

Yes. Pass generated content and a filename to .selectFile(), or use a buffer when the bytes must remain binary.

Can a direct API download prove the browser download button works?

No. A request verifies the endpoint response. To cover the browser action, trigger the app’s download control and inspect the resulting file in downloadsFolder.

Which command should I use for files created during a test?

Use cy.readFile() for changing or newly created files. Fixtures are cached for the selected path and encoding.

Official Cypress references