Resource library

QA How-To

How to Use Cypress file upload (2026)

Learn Cypress file upload with selectFile, fixtures, buffers, hidden inputs, drag and drop, multiple files, validation, network checks, and CI-safe patterns.

23 min read | 3,160 words

TL;DR

Chain .selectFile() from a file input, associated label, or drag target. Use a project-relative path for existing files, a descriptor with Cypress.Buffer for generated files, and assert the upload request plus the final application state.

Key Takeaways

  • Use the built-in selectFile command instead of the deprecated cypress-file-upload plugin.
  • Select existing files by project-relative path and generate small dynamic files with Cypress.Buffer.
  • Target the native input or associated label, and use force only for intentionally hidden controls.
  • Register network intercepts before selection when uploads begin on the input change event.
  • Verify browser selection, transport, processing, and the final user-visible result at appropriate layers.
  • Keep upload fixtures small, synthetic, sanitized, documented, and subject to deterministic cleanup.

Cypress file upload testing uses the built-in .selectFile() command. Chain it from an <input type="file">, its associated label, a drag target, or the document, then assert both the browser state and the application outcome. The older cypress-file-upload plugin is deprecated for this purpose and is unnecessary in current Cypress.

Uploading is more than attaching bytes. A production-quality test must decide whether to verify input selection, client validation, multipart network traffic, server processing, or the final user-visible result. This guide builds that strategy with current 2026 APIs and TypeScript examples.

TL;DR

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

cy.contains('resume.pdf').should('be.visible')
cy.contains('Upload complete').should('be.visible')
Scenario Recommended call
Existing file on disk .selectFile('cypress/fixtures/file.pdf')
Generated text or bytes .selectFile({ contents, fileName, mimeType })
Native multi-file input .selectFile([fileA, fileB])
Drag-and-drop zone .selectFile(file, { action: 'drag-drop' })
Hidden native input Target it precisely, then consider { force: true }
Upload request assertion Register cy.intercept() before selection

The best Cypress file upload test verifies a meaningful outcome. Confirming input.files.length is useful diagnostics, but it does not prove the server accepted or processed the file.

1. How Cypress File Upload Works in 2026

.selectFile() simulates a user selecting one or more files in an HTML file input, or dropping files onto a target. Cypress added it as a built-in command in version 9.3.0. Current suites should use this command rather than installing the former community upload plugin.

For the normal select action, the subject must resolve to one file input or a label associated with one. Cypress enforces actionability, waits for a path to exist, and fails when required conditions are not met. The command yields the same subject, but Cypress documents it as unsafe to chain later commands that depend on that subject. Re-query the input or assert application state instead.

it('selects a profile image', () => {
  cy.visit('/settings/profile')

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

  cy.get('input[name="avatar"]').then(($input) => {
    const file = $input[0].files?.[0]
    expect(file?.name).to.eq('avatar.png')
    expect(file?.type).to.eq('image/png')
  })
})

The path is relative to the Cypress project root, not the spec file and not necessarily the fixture folder. A standard cypress/fixtures/... path is explicit and portable.

Selection triggers the browser events expected by upload controls. Cypress does not bypass your application's JavaScript, file type checks, preview generation, or form submission behavior. That is why .selectFile() is a closer test of real client behavior than assigning a filename string.

For a complete Cypress foundation before upload-specific work, review the Cypress testing tutorial.

2. Set Up Realistic, Safe Upload Fixtures

Create small synthetic files that represent the contracts your product supports. Name each fixture after the boundary it exercises, not after a ticket or developer.

cypress/
  fixtures/
    documents/
      resume-valid.pdf
      resume-empty.pdf
      notes-valid.txt
      users-valid.csv
      users-invalid-header.csv
    images/
      avatar-valid.png
      avatar-too-wide.png
  e2e/
    document-upload.cy.ts

Fixtures should be reviewable, free of personal data, and stable over time. Do not download a customer's failed document and commit it. Build an equivalent synthetic sample with reserved names and addresses. Inspect image metadata and document properties because they can retain author names, device identifiers, or hidden content.

A tiny file is usually sufficient for functional behavior. Size-limit tests need boundary-specific files, but avoid keeping huge archives in the repository unless the product risk justifies them. Uploading a large file with a path is preferable to loading it first with cy.fixture(), because fixture loading transfers and caches the entire content in the Cypress runner.

cy.get('input[name="archive"]')
  .selectFile('cypress/fixtures/documents/archive-boundary.zip')

Track the reason for every binary fixture. A short README beside the files can record expected MIME type, dimensions, generation method, and the requirement covered. This helps reviewers determine whether a changed binary is intentional.

Keep extensions truthful for ordinary success cases. Create misleading filename or MIME combinations only in explicit security or validation scenarios. A fixture named valid.pdf should not secretly contain text, or failures become difficult to interpret.

3. Use Cypress File Upload on a Native Input

Start with a locator that identifies the actual upload control. Accessible labels are ideal when the page exposes them.

<label for="resume">Upload resume</label>
<input id="resume" name="resume" type="file" accept=".pdf" />
describe('resume upload', () => {
  it('accepts a PDF resume', () => {
    cy.intercept('POST', '/api/resumes').as('uploadResume')
    cy.visit('/resumes/new')

    cy.contains('label', 'Upload resume')
      .selectFile('cypress/fixtures/documents/resume-valid.pdf')

    cy.contains('button', 'Submit').click()

    cy.wait('@uploadResume').then(({ request, response }) => {
      expect(request.headers['content-type']).to.include('multipart/form-data')
      expect(response?.statusCode).to.eq(201)
    })

    cy.contains('Resume uploaded').should('be.visible')
  })
})

Register the intercept before selection when the application uploads immediately after a change event. Registering it only before clicking Submit is too late for auto-upload widgets.

Do not manually set a multipart content-type header in the browser test. The browser generates the multipart boundary. Overriding the header without a matching body boundary can create a malformed request that differs from real user behavior.

The accept attribute guides the file picker but is not a security boundary. .selectFile() can still exercise server and client handling of disallowed content. The server must validate type, size, content, permissions, and storage rules independently.

Prefer an application-visible assertion after the network check. The response status proves transport, while the confirmation, preview, or new document row proves the UI handled the response.

4. Handle Hidden Inputs and Custom Upload Buttons

Design systems often render a styled button while keeping the native input visually hidden. If clicking the visible control opens the operating-system picker, Cypress cannot automate that native dialog. Target the underlying input with .selectFile().

<label className="upload-button" htmlFor="evidence">
  Choose evidence
</label>
<input
  id="evidence"
  data-cy="evidence-input"
  type="file"
  className="sr-only"
/>
cy.get('[data-cy="evidence-input"]')
  .selectFile('cypress/fixtures/images/screenshot.png', { force: true })

cy.contains('screenshot.png').should('be.visible')

Use { force: true } only when the native input is deliberately hidden and the visible label is the user interaction surface. It disables actionability checks, so a broad selector could attach a file to the wrong or disabled control. Keep the locator specific and add a visible-state assertion.

If the label is properly associated with the input, Cypress also permits chaining from the label. This can express the accessible contract:

cy.contains('label', 'Choose evidence')
  .selectFile('cypress/fixtures/images/screenshot.png')

Do not add arbitrary waits for animations. Cypress already waits for the selected subject to satisfy actionability rules. If a modal transition temporarily covers the input, assert the dialog is visible and target the stable input inside it.

When a component has no native input and no standards-based drop behavior, question its accessibility and testability. A user still needs a browser-supported mechanism. Coordinate with developers on an accessible label, input, and status region rather than building a brittle test around implementation-only event handlers.

5. Test Drag-and-Drop Upload Zones

Use { action: 'drag-drop' } to simulate dropping files. Chain from the element whose drag events the application handles.

it('uploads evidence through drag and drop', () => {
  cy.intercept('POST', '/api/evidence').as('uploadEvidence')
  cy.visit('/claims/42')

  cy.get('[data-cy="evidence-dropzone"]').selectFile(
    'cypress/fixtures/images/screenshot.png',
    { action: 'drag-drop' },
  )

  cy.wait('@uploadEvidence')
    .its('response.statusCode')
    .should('eq', 201)

  cy.get('[data-cy="evidence-list"]')
    .should('contain.text', 'screenshot.png')
})

Some applications attach drop listeners to the entire document. Current Cypress supports this pattern:

cy.document().selectFile(
  'cypress/fixtures/documents/resume-valid.pdf',
  { action: 'drag-drop' },
)

Choose the smallest target that represents the real interaction. Dropping on document when the app handles only a specific zone can let an overly broad handler hide an accessibility or routing defect.

A drag-and-drop test should verify more than file appearance. Assert the dropzone's error state for rejected files, the queued state before submission, progress or completion behavior if meaningful, and the final saved record. Avoid pixel-specific drag hover assertions unless styling is a business requirement.

The drag sequence is simulated, not a physical operating-system gesture. It validates application event handling within the browser. Keep one native input path and one drag path when both are customer-facing and implemented differently.

6. Upload Multiple Files Correctly

Pass an array to .selectFile() for multiple files. With the default select action, the input must have the multiple attribute.

it('queues two attachments', () => {
  cy.visit('/tickets/new')

  cy.get('input[name="attachments"]').selectFile([
    'cypress/fixtures/documents/notes-valid.txt',
    'cypress/fixtures/images/screenshot.png',
  ])

  cy.get('[data-cy="attachment-row"]').should('have.length', 2)
  cy.contains('[data-cy="attachment-row"]', 'notes-valid.txt')
  cy.contains('[data-cy="attachment-row"]', 'screenshot.png')
})

Verify ordering only if the application promises it. Some servers or upload libraries process files concurrently, so completion order may differ from selection order. A set-based assertion is more stable when order has no business meaning.

Test mixed acceptance deliberately. If one of two files is invalid, determine the required product behavior: reject the whole batch, accept the valid file and explain the rejection, or block submission until the invalid item is removed. Express that contract in assertions.

cy.get('input[name="attachments"]').selectFile([
  'cypress/fixtures/documents/notes-valid.txt',
  'cypress/fixtures/documents/users-invalid-header.csv',
])

cy.contains('users-invalid-header.csv')
  .parents('[data-cy="attachment-row"]')
  .should('contain.text', 'Unsupported file')

Avoid relying on parents() across a fragile component tree when a stable attachment row selector can be queried by content. The main principle is scoping the error to the file that caused it.

Bulk upload tests can become slow. Cover representative batch behavior in the browser, then validate larger volume limits at the API or service layer.

7. Generate File Contents Inside the Test

Not every case needs a committed fixture. For small text, CSV, or JSON payloads, create contents with Cypress.Buffer.from() and specify metadata.

const csv = [
  'email,role',
  'avery@example.test,viewer',
  'jordan@example.test,editor',
].join('\n')

cy.get('input[name="users"]').selectFile({
  contents: Cypress.Buffer.from(csv),
  fileName: 'users.csv',
  mimeType: 'text/csv',
  lastModified: new Date('2026-01-15T00:00:00Z').valueOf(),
})

This is useful for parameterized boundaries and avoids a directory full of almost-identical files. Keep the generated content readable. A long binary expression inside a spec is harder to review than a fixture.

You can generate JSON while preserving exact text:

const manifest = JSON.stringify(
  {
    project: 'checkout-reliability',
    owners: ['qa', 'platform'],
  },
  null,
  2,
)

cy.get('input[name="manifest"]').selectFile({
  contents: Cypress.Buffer.from(manifest),
  fileName: 'manifest.json',
  mimeType: 'application/json',
})

Cypress can infer metadata from a path, but generated bytes have no inherent filename. Set fileName and mimeType whenever the application uses them.

Do not use a filename extension as proof of file content. A server that accepts user uploads needs independent content validation. Browser tests can verify the customer-facing rejection, while security, service, and unit tests should cover deeper parser and malware-scanning behavior.

Generated timestamps should be fixed when assertions depend on them. Defaulting to the current time is fine when time is irrelevant, but a deterministic lastModified makes metadata tests repeatable.

8. Assert the Upload Request and Server Result

A request alias adds observability, but multipart bodies are not always convenient to inspect as plain JSON. Focus browser-level assertions on durable contract signals: method, URL, content type, response status, and user-visible outcome.

it('sends the attachment to the selected ticket', () => {
  cy.intercept('POST', '/api/tickets/42/attachments').as('uploadAttachment')
  cy.visit('/tickets/42')

  cy.get('input[name="attachment"]')
    .selectFile('cypress/fixtures/documents/notes-valid.txt')

  cy.wait('@uploadAttachment').then(({ request, response }) => {
    expect(request.headers['content-type']).to.match(/^multipart\/form-data;/)
    expect(response?.statusCode).to.eq(201)
    expect(response?.body).to.include({
      fileName: 'notes-valid.txt',
      status: 'ready',
    })
  })

  cy.contains('[data-cy="attachment-row"]', 'notes-valid.txt')
    .should('contain.text', 'Ready')
})

If the UI uses a presigned upload flow, expect several boundaries: request an upload URL, send bytes to object storage, then notify the application service. Alias each owned call with a meaningful name. Stub third-party storage only at an owned contract boundary when deterministic UI behavior is the test goal.

A successful HTTP response might mean only "accepted for processing." For virus scanning, parsing, transcription, or thumbnail generation, the final state may arrive later. Wait on the application's status request or event-driven UI transition, not an arbitrary number of seconds.

The Cypress cy.intercept guide provides deeper route matching patterns. Keep upload-specific assertions tied to product behavior rather than decoding multipart internals in every E2E test.

9. Test Validation and Failure States

Cover representative client and server failures: unsupported type, file too large, empty file, malformed content, duplicate filename, expired upload URL, permission failure, and server error. Assign each boundary to the most efficient test layer.

it('shows a useful error when the upload service rejects a file', () => {
  cy.intercept('POST', '/api/resumes', {
    statusCode: 413,
    body: {
      code: 'FILE_TOO_LARGE',
      message: 'The file exceeds the allowed size',
    },
  }).as('uploadResume')

  cy.visit('/resumes/new')
  cy.get('input[name="resume"]')
    .selectFile('cypress/fixtures/documents/resume-valid.pdf')

  cy.wait('@uploadResume')
  cy.contains('This file is too large').should('be.visible')
  cy.contains('Choose another file').should('be.visible')
})

A stub proves the frontend handles the server contract. It does not prove the real service enforces the limit. Add direct API tests for exact byte boundaries and integrated checks for the most important path.

For client validation, assert that no upload request occurs only when the application contract guarantees local rejection. One approach is to spy on the route, select an invalid file, and verify the visible error. Be cautious with negative network assertions because a request may fire after the assertion. Prefer an application signal that validation has completed.

Test recovery. After a rejected file, selecting a valid replacement should clear the error and upload successfully. Error states that persist are common defects and easy to miss when every test stops at rejection.

Use exact, helpful messages when wording is part of the product contract. Otherwise assert a stable error region and error code-driven behavior rather than punctuation.

10. Test Large Files, Progress, and Cancellation Without Fragility

Large upload scenarios consume repository space, runner memory, network time, and CI storage. Use a layered strategy. Test exact size validation at the service level, a representative near-boundary file in one browser scenario, and UI progress behavior with controlled network conditions.

For a path-based upload, Cypress reads the file as needed for selection. Do not first load a large binary with cy.fixture(), which caches and transfers the entire value into the runner command chain.

Progress bars are difficult to assert with cy.intercept() stubs because a static delay does not reproduce real streaming progress. Verify stable states such as queued, uploading, completed, failed, and canceled. Unit or component tests can drive precise progress callbacks.

it('allows an in-progress upload to be canceled', () => {
  cy.visit('/media')

  cy.get('input[name="media"]')
    .selectFile('cypress/fixtures/documents/video-sample.mp4')

  cy.contains('Uploading').should('be.visible')
  cy.contains('button', 'Cancel upload').click()

  cy.contains('Upload canceled').should('be.visible')
  cy.get('[data-cy="media-row"]').should('not.exist')
})

The exact example requires an application that keeps the upload pending long enough. In a real suite, control the backend test endpoint or component transport rather than racing a fast local request.

Do not claim performance results from Cypress E2E timing alone. Browser, CI machine, proxy, storage service, and network all contribute. Use dedicated performance tooling for throughput and sustained concurrency.

11. Keep Cypress File Upload Tests Secure

Uploaded files are untrusted input. Functional E2E coverage should complement, not replace, security controls. Verify user-facing behavior for disallowed extensions, MIME mismatches, dangerous names, oversized content, and permission failures. Validate the enforcement itself at service and security-test layers.

Use synthetic payloads. Never commit malware, real personal documents, private keys, proprietary customer archives, or active exploit samples into an ordinary test repository. Security teams may maintain quarantined samples under separate controls.

Test filename rendering safely. A filename containing HTML-like text should appear as text, not execute. Use a benign string that demonstrates encoding without carrying an active payload. Verify that path-like names are normalized by the server and never trusted as storage paths.

Clean up uploaded objects through a supported API or isolated test tenant. Randomized filenames avoid collisions, but unbounded random artifacts create storage and privacy problems. Prefer a run identifier plus deterministic cleanup.

Credentials used for upload tests should access only the test bucket or tenant. Presigned URLs should have limited scope and lifetime. Avoid logging full storage URLs when their query parameters contain authorization material.

For broader input-risk thinking, connect upload tests with an API testing strategy so browser, service, contract, and security checks have explicit ownership.

12. Debug Cypress File Upload Failures Systematically

First classify the failure: selector, actionability, path, metadata, browser validation, network, server processing, or final UI rendering.

If Cypress says the file does not exist, remember that the path is relative to the project root. Confirm filename case, especially when local macOS runs pass but a case-sensitive Linux CI filesystem fails. Keep exact casing in imports and fixture paths.

If the element is not a file input, inspect the DOM. The visible button may only activate a hidden input. Target the associated label or native input. Use force only for intentional hiding, not to ignore a disabled state.

If the server rejects the type, inspect file.name, file.type, and the fixture bytes. For generated content, specify fileName and mimeType. For a path, make sure the extension reflects the actual content.

If cy.wait('@upload') times out, register the intercept before selecting the file, check whether upload starts on selection or form submission, and match the real method plus URL. A presigned flow may upload to a different host.

If the network succeeds but the row remains pending, look for a status polling request or asynchronous job. Wait on a meaningful alias or UI state change. Do not expand fixed waits.

Finally, reproduce the failure with one small fixture and one spec in open mode. Inspect the Command Log, browser console, request timeline, server logs available to the test environment, and the saved object metadata without printing sensitive URLs.

13. Review the Upload Architecture Before Automating

Map the upload sequence from browser to durable result before choosing assertions. A simple form may send one multipart request. A cloud-storage flow may request authorization, upload bytes to a signed URL, confirm completion, and wait for scanning or parsing. These are materially different workflows even when the page shows the same button.

Draw the owned boundaries and name their outcomes. For each request, record who initiates it, whether the test should use the real dependency, the safe data needed, and the final customer state. This prevents a test from waiting on the first 201 while the file is still unusable.

Use stubs selectively. A controlled response is excellent for a rare expired session, permission error, or parser failure. It is weak evidence for storage integration. Keep at least one critical happy path against the real test services, and pair it with direct contracts for every backend boundary. When a third-party provider is involved, stub at the interface your organization owns.

Clarify cleanup before running in parallel. Identify which service owns deletion, whether background jobs retain derived artifacts, and how a test run finds everything it created. A database row may be deleted while the original object, thumbnail, scan result, and audit event remain. Test-environment cleanup needs the same architecture awareness as creation.

Finally, decide what failure artifacts are safe. Screenshots can show document previews, videos can capture filenames, and network diagnostics can expose signed URLs. Use synthetic content, redact sensitive query parameters, restrict artifact access, and define retention. A well-designed upload test protects data while giving engineers enough evidence to locate the failing boundary.

Interview Questions and Answers

Q: What command should you use for Cypress file upload?

Use the built-in .selectFile() command. The older upload plugin is no longer needed for standard HTML inputs and drag-and-drop targets.

Q: How do you upload to a hidden input?

Target the native input or its associated label. Use { force: true } only when the input is deliberately hidden behind an accessible custom control.

Q: How do you test drag and drop?

Chain .selectFile() from the drop target and pass { action: 'drag-drop' }. Then assert the queued or uploaded application state.

Q: How do you select several files?

Pass an array of paths or file descriptors. For the default select action, the native input must have the multiple attribute.

Q: Why is checking input.files not enough?

It proves browser selection but not that the application sent, accepted, processed, and displayed the upload. Add network and user-visible outcome assertions.

Q: How do you test a large-file rejection?

Use direct API tests for precise byte boundaries and one representative browser test for the user-facing message and recovery. Avoid huge fixtures throughout the E2E suite.

Q: What should you assert for multipart upload?

At the browser layer, assert the endpoint, method, multipart content type, response contract, and final product state. Detailed multipart parsing usually belongs in service-level tests.

Common Mistakes

  • Installing the deprecated upload plugin when .selectFile() already covers the scenario.
  • Calling .click() and trying to automate the native operating-system file picker.
  • Using { force: true } with a broad selector that can target the wrong input.
  • Resolving file paths relative to the spec instead of the project root.
  • Loading a large binary with cy.fixture() before selecting it by alias.
  • Registering cy.intercept() after an auto-upload already started.
  • Manually setting multipart content-type without a correct boundary.
  • Checking only input.files.length and calling the upload successful.
  • Assuming the accept attribute provides server-side security.
  • Verifying exact completion order for concurrent files when order is not promised.
  • Using customer documents or unsanitized binary fixtures.
  • Adding fixed waits for scanning, parsing, or progress updates.
  • Leaving uploaded artifacts in a shared test tenant.

Conclusion

Cypress file upload testing in 2026 starts with the built-in .selectFile() command and ends with a meaningful application result. Use paths for stable fixtures, buffers for small generated cases, arrays for native multiple inputs, and action: 'drag-drop' for drop zones.

Keep fixtures synthetic and focused, register network observation before the upload trigger, cover recovery as well as rejection, and distribute exact validation across browser, API, component, and security layers. Your next step is to choose one critical upload flow and add assertions for selection, transport, processing, and final UI state.

Interview Questions and Answers

How does file upload work in current Cypress?

I use the built-in .selectFile() command on a native input, associated label, or drag target. It can take project-relative paths, aliases, buffers, descriptors, or arrays. I then assert the network and user-visible result.

When would you use force with selectFile?

Only when the native input is intentionally hidden behind an accessible custom control. I keep the selector precise because force bypasses actionability checks, and I verify the visible filename or status afterward.

How do you test an auto-upload widget?

I register cy.intercept before selecting the file because the change event may send the request immediately. I wait on the named request and assert both its response and the final UI state.

How do you test drag-and-drop uploads?

I call selectFile with action set to drag-drop on the real drop target. I assert queued, rejected, or completed behavior and keep a separate input-path test if the product supports both mechanisms.

What is your strategy for large file testing?

I validate exact size boundaries through service tests and keep only representative E2E coverage. I select large files by path rather than loading them with cy.fixture, and I use dedicated performance tools for throughput.

What makes an upload test production quality?

It identifies the intended boundary and checks more than input.files. It covers transport or processing where relevant, validates the final user result, tests recovery from errors, uses sanitized data, and cleans up server artifacts.

How do you secure file upload test data?

I use synthetic files without personal or proprietary content, inspect metadata, avoid active exploit samples, and store artifacts in an isolated tenant. Upload credentials have least privilege and cleanup is deterministic.

Frequently Asked Questions

How do I upload a file in Cypress?

Chain .selectFile('path/to/file') from an input with type=file or its associated label. The path is relative to the Cypress project root.

Do I need cypress-file-upload plugin?

No for standard modern use cases. Cypress has a built-in .selectFile() command for native file inputs, multiple files, generated contents, and drag-and-drop simulation.

How do I upload a file to a hidden input in Cypress?

Select the precise native input or associated label. If the input is intentionally hidden behind a custom accessible control, pass { force: true } and verify the visible application state afterward.

How do I drag and drop a file in Cypress?

Call .selectFile(file, { action: 'drag-drop' }) on the element that handles drop events. Use cy.document().selectFile(...) only when the application intentionally handles document-level drops.

Can Cypress upload multiple files?

Yes. Pass an array of file paths or descriptors to .selectFile(). For the default select action, the underlying input must include the multiple attribute.

How do I create an upload file dynamically?

Create contents with Cypress.Buffer.from() and pass an object containing contents, fileName, mimeType, and optionally lastModified. This is best for small readable text, CSV, or JSON cases.

How should I test maximum upload size?

Test exact byte boundaries at the API or service layer, then keep one representative browser scenario for the validation message and recovery flow. Avoid distributing huge binary fixtures through the E2E suite.

Related Guides