File upload testing with Cypress

selectFile for inputs and drop zones, in-memory files, intercepting the upload request and checking downloads.

Published by SampleTestFiles. About 3 minutes to read.

Cypress has had built-in file upload support since version 9.3 through the selectFile command, so no plugin is needed. This guide shows the patterns that cover nearly every upload feature.

Where to put fixtures

Cypress looks for fixtures in cypress/fixtures. Download a few small samples there: the 10 KB PDF, the 1 KB PNG and the 10 KB CSV are a good start.

cypress/
  fixtures/
    sample-pdf-10kb.pdf
    sample-png-1kb.png
    sample-csv-10kb.csv
  e2e/
    upload.cy.js

Select a file

Paths passed to selectFile are relative to the project root.

describe('document upload', () => {
  it('uploads a PDF', () => {
    cy.visit('/documents');
    cy.get('input[type=file]').selectFile('cypress/fixtures/sample-pdf-10kb.pdf');
    cy.contains('button', 'Upload').click();
    cy.contains('li', 'sample-pdf-10kb.pdf').should('be.visible');
  });
});

For several files, pass an array. The input must have the multiple attribute.

cy.get('input[type=file]').selectFile([
  'cypress/fixtures/sample-pdf-10kb.pdf',
  'cypress/fixtures/sample-png-1kb.png',
]);

Hidden inputs

When the real input is hidden behind a styled button, Cypress refuses to act on it by default. Pass force: true:

cy.get('input[type=file]').selectFile('cypress/fixtures/sample-pdf-10kb.pdf', { force: true });

You can also call selectFile on the label that wraps the input, which is closer to what a user does.

Drag and drop

To test a drop zone, call selectFile on the drop target with the drag-drop action:

cy.get('[data-cy=drop-zone]').selectFile('cypress/fixtures/sample-png-1kb.png', {
  action: 'drag-drop',
});

Cypress dispatches the drag events with a DataTransfer object containing the file.

Files created in the test

A file can be built in memory by passing an object with its contents. This is useful for size limits and odd cases.

it('rejects files over 5 MB', () => {
  cy.visit('/documents');
  cy.get('input[type=file]').selectFile({
    contents: Cypress.Buffer.alloc(5 * 1024 * 1024 + 1),
    fileName: 'too-large.bin',
    mimeType: 'application/octet-stream',
    lastModified: Date.now(),
  });
  cy.get('[role=alert]').should('contain', '5 MB');
});

A buffer of zeros is fine for a size check. If the application validates content, use a real sample file of the right size instead.

A zero-byte file and a mislabelled file take one line each:

cy.get('input[type=file]').selectFile({ contents: Cypress.Buffer.alloc(0), fileName: 'empty.txt' });

cy.get('input[type=file]').selectFile({
  contents: 'cypress/fixtures/sample-png-1kb.png',
  fileName: 'not-really.pdf',
  mimeType: 'application/pdf',
});

The second example renames a PNG to look like a PDF, which tests whether the server checks content or trusts the name.

Assert on the request

The most reliable upload assertion is on the network request itself. Intercept it, perform the upload, then check the response.

it('sends the file to the API', () => {
  cy.intercept('POST', '/api/documents').as('upload');

  cy.visit('/documents');
  cy.get('input[type=file]').selectFile('cypress/fixtures/sample-pdf-10kb.pdf');
  cy.contains('button', 'Upload').click();

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

The last assertion assumes your API returns the stored size. Comparing it with the sample's published byte count confirms that the whole file arrived.

Upload without the user interface

To seed data before a test, send the file with cy.request. Read it as binary and wrap it in FormData:

cy.fixture('sample-csv-10kb.csv', 'binary').then((content) => {
  const blob = Cypress.Blob.binaryStringToBlob(content, 'text/csv');
  const form = new FormData();
  form.append('file', blob, 'sample-csv-10kb.csv');

  cy.request({ method: 'POST', url: '/api/imports', body: form }).its('status').should('eq', 201);
});

Verify a download

Cypress saves downloads to cypress/downloads by default. Click the link, then read the file. cy.readFile retries until the file exists.

it('downloads the original file', () => {
  cy.visit('/documents');
  cy.contains('a', 'Download').click();

  cy.readFile('cypress/downloads/sample-pdf-10kb.pdf', null).then((buffer) => {
    expect(buffer.length).to.eq(10240);
  });
});

Passing null as the encoding returns a raw buffer, so the length is the exact file size in bytes.

A data-driven validation spec

const cases = [
  { file: 'sample-pdf-10kb.pdf', accepted: true },
  { file: 'sample-png-1kb.png', accepted: false },
  { file: 'empty-file.txt', accepted: false },
];

cases.forEach(({ file, accepted }) => {
  it(`${file} is ${accepted ? 'accepted' : 'rejected'}`, () => {
    cy.visit('/documents');
    cy.get('input[type=file]').selectFile(`cypress/fixtures/${file}`);
    cy.contains('button', 'Upload').click();
    cy.get(accepted ? '[role=status]' : '[role=alert]').should('be.visible');
  });
});

Extend the list with the zero-byte file and a file with a non-ASCII name. Both are quick to add and regularly find bugs.

Things that go wrong

  • An error saying that selectFile needs a file input, a label or the drag-drop action. The selector matched a button or a wrapper. Target the input, its label, or use the drag-drop action.
  • The file path is not found. Paths are relative to the project root, not to the spec file.
  • Old downloads cause false passes. Cypress clears the downloads folder before a run by default; keep trashAssetsBeforeRuns enabled so that a stale file cannot satisfy an assertion.

Files used in this guide

Sample files referred to in this guide
FileFormatSizeContentsDownload
10 KB PDF samplesample-pdf-10kb.pdf PDF 10 KB10,240 bytes Pages: 2 Download PDF
1 KB PNG samplesample-png-1kb.png PNG 1 KB1,024 bytes 21 × 14 px Download PNG
10 KB CSV samplesample-csv-10kb.csv CSV 10 KB10,240 bytes Data rows: 78 Download CSV
Empty file (0 bytes)empty-file.txt TXT 0 B0 bytes Lines: 0 Download TXT
File name with non-ASCII characterssample-ünïcödé-文件-имя.txt TXT 75 B75 bytes File name scripts: Latin with diacritics, Han, Cyrillic Download TXT

Related guides