72

How can I write an e2e test of flow that requires interaction with the file Input DOM element?

If it's a text input I can interact with it (check value, set value) etc as its a DOM component. But If I have a File Input element, I am guessing that the interaction is limited till I can open the dialog to select a File. I can't move forward and select the file I want to upload as the dialog would be native and not some browser element.

So how would I test that a user can correctly upload a file from my site? I am using Cypress to write my e2e tests.

sidoshi
  • 2,040
  • 2
  • 15
  • 30

13 Answers13

69

NB : latest version of cypress supports selectFile - see other answer

it('Testing picture uploading', () => {
    cy.fixture('testPicture.png').then(fileContent => {
        cy.get('input[type="file"]').attachFile({
            fileContent: fileContent.toString(),
            fileName: 'testPicture.png',
            mimeType: 'image/png'
        });
    });
});

Use cypress file upload package: https://www.npmjs.com/package/cypress-file-upload

Note: testPicture.png must be in fixture folder of cypress

Patrick
  • 8,175
  • 7
  • 56
  • 72
Muhammad Bilal
  • 1,840
  • 1
  • 18
  • 16
  • 5
    Had trouble getting this to work. It seems like the code changed. Instead of `cy.get( ... ).upload()`, the function is now called `cy.get( ... ).attachFile()`. I have edited the original answer. – Jules Colle Jun 21 '20 at 11:03
  • 1
    Yes Jules Colle, I just checked the official documentation and yes you are right, .upload has been changed to .attachFile. Thanks – Muhammad Bilal Jun 23 '20 at 06:22
  • 2
    I had trouble as well. In order to fix it I had to chain then statements together like `cy.fixture(...).then( fc => {return Cypress.Blob.base64StringToBlob( fc ) }).then(fileContentasBlob => { cy.get('input[type="file"]').attachFile({ ...... ` – Chadd Frasier Jul 20 '20 at 22:03
  • 1
    I had trouble with this approach. The upload worked, but the server could not process the uploaded image: (`PIL.UnidentifiedImageError: cannot identify image file`). I was able to avoid this error using [Lucas Andrade's approach](https://stackoverflow.com/questions/47074225/how-to-test-file-inputs-with-cypress/62204487#62204487) with `cy.get('['input[type="file"]']').attachFile(fixtureFile)` only (without information about mimeType etc.). – Jasmin Jan 20 '21 at 16:36
  • you do not need fixture, but you have to provide valid **fileContent**, that is better when you know, for example csv content - no need even to create source file, and file path could be faked: ` cy.get(input_name).attachFile({ fileContent: csv_content, fileName: csv_path, mimeType: 'text/csv' });` – Sasha Bond Feb 19 '21 at 21:23
  • 1
    Note that as of 9.3.0 this functionality is natively supported via cy.selectFile and there's a migration guide on cypress' website from this plugin – Alex Beardsley Jan 19 '22 at 19:50
  • This is outdated - selectFile discussed below - please delete – Patrick Apr 11 '23 at 09:50
28

For me the easier way to do this is using this cypress file upload package

Install it:

npm install --save-dev cypress-file-upload

Then add this line to your project's cypress/support/commands.js:

import 'cypress-file-upload';

Now you can do:

const fixtureFile = 'photo.png';
cy.get('[data-cy="file-input"]').attachFile(fixtureFile);

photo.png must be in cypress/fixtures/

For more examples checkout the Usage section on README of the package.

Lucas Andrade
  • 4,315
  • 5
  • 29
  • 50
  • 2
    Wow! I tried all the (more complicated) examples from the package's README, but this one – the simplest of all – is the only one that works! Thank you! – Adrien Joly Sep 05 '20 at 16:10
  • this works great for me ! For all scenarious, however when i try to use clientside jQuery validation on the files[] array. It can't validate the file type. I have validation for checking you can't upload anything other than image files. But when i specify a mimetupe in `attachfile' the file upload file that goes to the server is null ? – Gweaths May 10 '21 at 15:54
  • This solution worked (had to get additional details from npm package author linked in answer), but in order for my intellisense to recognize 'attachFile', I had to add ` /// ` to the top of the spec file. – tengen Nov 15 '21 at 19:31
  • Use @thisismydesign solution! – ismaestro Mar 31 '22 at 11:44
  • This is outdated - selectFile discussed below - please delete – Patrick Apr 11 '23 at 09:50
21

With this approach/hack you can actually make it: https://github.com/javieraviles/cypress-upload-file-post-form

It is based on different answers from the aformentioned thread https://github.com/cypress-io/cypress/issues/170

First scenario (upload_file_to_form_spec.js):

I want to test a UI where a file has to be selected/uploaded before submitting the form. Include the following code in your "commands.js" file within the cypress support folder, so the command cy.upload_file() can be used from any test:
Cypress.Commands.add('upload_file', (fileName, fileType, selector) => {
    cy.get(selector).then(subject => {
        cy.fixture(fileName, 'hex').then((fileHex) => {

            const fileBytes = hexStringToByte(fileHex);
            const testFile = new File([fileBytes], fileName, {
                type: fileType
            });
            const dataTransfer = new DataTransfer()
            const el = subject[0]

            dataTransfer.items.add(testFile)
            el.files = dataTransfer.files
        })
    })
})

// UTILS
function hexStringToByte(str) {
    if (!str) {
        return new Uint8Array();
    }

    var a = [];
    for (var i = 0, len = str.length; i < len; i += 2) {
        a.push(parseInt(str.substr(i, 2), 16));
    }

    return new Uint8Array(a);
}

Then, in case you want to upload an excel file, fill in other inputs and submit the form, the test would be something like this:

describe('Testing the excel form', function () {
    it ('Uploading the right file imports data from the excel successfully', function() {

    const testUrl = 'http://localhost:3000/excel_form';
    const fileName = 'your_file_name.xlsx';
    const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
    const fileInput = 'input[type=file]';

    cy.visit(testUrl);
    cy.upload_file(fileName, fileType, fileInput);
    cy.get('#other_form_input2').type('input_content2');
    .
    .
    .
    cy.get('button').contains('Submit').click();

    cy.get('.result-dialog').should('contain', 'X elements from the excel where successfully imported');
})

})

Javier Aviles
  • 7,952
  • 2
  • 22
  • 28
14

Testing File Input elements is not yet supported in Cypress. The only way to test File Inputs is to:

  1. Issue native events (which Cypress has on their Roadmap).
  2. Understand how your application handles file uploads with File API and then stub it out. It's possible but not generic enough to give any specific advice on.

See this open issue for more detail.

Jennifer Shehane
  • 6,645
  • 1
  • 30
  • 26
8

In my case I had client & server side file validation to check if the file is JPEG or PDF. So I had to create a upload command which would read the file in binary from Fixtures and prepare a blob with the file extension.

Cypress.Commands.add('uploadFile', { prevSubject: true }, (subject, fileName, fileType = '') => {
  cy.fixture(fileName,'binary').then(content => {
    return Cypress.Blob.binaryStringToBlob(content, fileType).then(blob => {
      const el = subject[0];
      const testFile = new File([blob], fileName, {type: fileType});
      const dataTransfer = new DataTransfer();

      dataTransfer.items.add(testFile);
      el.files = dataTransfer.files;
      cy.wrap(subject).trigger('change', { force: true });
    });
  });
});

then use it as

cy.get('input[type=file]').uploadFile('smiling_pic.jpg', 'image/jpeg');

smiling_pic.jpg will be in fixtures folder

Vishwa
  • 300
  • 5
  • 16
  • 1
    This is the only example I could find that worked with my particular XML file format (otherwise encoding was weird). Only change is that `Cypress.Blob.binaryStringToBlob(...)` no longer returns Promise and now returns just Blob, meaning usage is now `const blob = Cypress.Blob.binaryStringToBlob(content, fileType);` More at [https://docs.cypress.io/api/utilities/blob](https://docs.cypress.io/api/utilities/blob) – Lesley.Oakey May 13 '21 at 14:56
  • Use @thisismydesign solution! – ismaestro Mar 31 '22 at 11:44
4

The following function works for me,

cy.getTestElement('testUploadFront').should('exist');

const fixturePath = 'test.png';
const mimeType = 'application/png';
const filename = 'test.png';

cy.getTestElement('testUploadFrontID')
  .get('input[type=file')
  .eq(0)
  .then(subject => {
    cy.fixture(fixturePath, 'base64').then(front => {
      Cypress.Blob.base64StringToBlob(front, mimeType).then(function(blob) {
        var testfile = new File([blob], filename, { type: mimeType });
        var dataTransfer = new DataTransfer();
        var fileInput = subject[0];

        dataTransfer.items.add(testfile);
        fileInput.files = dataTransfer.files;
        cy.wrap(subject).trigger('change', { force: true });
      });
    });
  });

// Cypress.Commands.add(`getTestElement`, selector =>
//   cy.get(`[data-testid="${selector}"]`)
// );
Vikki
  • 1,897
  • 1
  • 17
  • 24
3

Also based on previously mentioned github issue, so big thanks to the folks there.

The upvoted answer worked initially for me, but I ran into string decoding issues trying to handle JSON files. It also felt like extra work having to deal with hex.

The code below handles JSON files slightly differently to prevent encode/decode issues, and uses Cypress's built in Cypress.Blob.base64StringToBlob:

/**
 * Converts Cypress fixtures, including JSON, to a Blob. All file types are
 * converted to base64 then converted to a Blob using Cypress
 * expect application/json. Json files are just stringified then converted to
 * a blob (prevents issues with invalid string decoding).
 * @param {String} fileUrl - The file url to upload
 * @param {String} type - content type of the uploaded file
 * @return {Promise} Resolves with blob containing fixture contents
 */
function getFixtureBlob(fileUrl, type) {
  return type === 'application/json'
    ? cy
        .fixture(fileUrl)
        .then(JSON.stringify)
        .then(jsonStr => new Blob([jsonStr], { type: 'application/json' }))
    : cy.fixture(fileUrl, 'base64').then(Cypress.Blob.base64StringToBlob)
}

/**
 * Uploads a file to an input
 * @memberOf Cypress.Chainable#
 * @name uploadFile
 * @function
 * @param {String} selector - element to target
 * @param {String} fileUrl - The file url to upload
 * @param {String} type - content type of the uploaded file
 */
Cypress.Commands.add('uploadFile', (selector, fileUrl, type = '') => {
  return cy.get(selector).then(subject => {
    return getFixtureBlob(fileUrl, type).then(blob => {
      return cy.window().then(win => {
        const el = subject[0]
        const nameSegments = fileUrl.split('/')
        const name = nameSegments[nameSegments.length - 1]
        const testFile = new win.File([blob], name, { type })
        const dataTransfer = new win.DataTransfer()
        dataTransfer.items.add(testFile)
        el.files = dataTransfer.files
        return subject
      })
    })
  })
})
Scott
  • 1,575
  • 13
  • 17
2

You can do it with new Cypress command:

cy.get('input[type=file]').selectFile('file.json')

This is now available within Cypress library itself from version 9.3 and above. Follow the migration guide on how to move from cypress-file-upload plugin to Cypress .selectFile() command:

Migrating-from-cypress-file-upload-to-selectFile

t_dom93
  • 10,226
  • 1
  • 52
  • 38
0

in your commands.ts file within your test folder add:

//this is for typescript intellisense to recognize new command
declare namespace Cypress {
  interface Chainable<Subject> {
   attach_file(value: string, fileType: string): Chainable<Subject>;
  }
}

//new command
Cypress.Commands.add(
  'attach_file',
{
  prevSubject: 'element',
},
(input, fileName, fileType) => {
    cy.fixture(fileName)
      .then((content) => Cypress.Blob.base64StringToBlob(content, fileType))
      .then((blob) => {
        const testFile = new File([blob], fileName);
        const dataTransfer = new DataTransfer();

        dataTransfer.items.add(testFile);
        input[0].files = dataTransfer.files;
        return input;
      });
  },
);

Usage:

cy.get('[data-cy=upload_button_input]')
      .attach_file('./food.jpg', 'image/jpg')
      .trigger('change', { force: true });

another option is to use cypress-file-upload, which is buggy in version 4.0.7 (uploads files twice)

niio
  • 326
  • 3
  • 15
0
cy.fixture("image.jpg").then((fileContent) => {
   cy.get("#fsp-fileUpload").attachFile({
      fileContent,
      fileName: "image",
      encoding: "base64",
      mimeType: "image/jpg",
    });
  });
Ben Ahlander
  • 1,113
  • 11
  • 12
0

Here is the multiple file upload version:

Cypress.Commands.add('uploadMultiFiles',(args) => {
  const { dataJson, dirName, inputTag, mineType} = args
  const arr = []
  dataJson.files.forEach((file, i) => {
    cy.fixture(`${ dirName + file }`).as(`file${i}`)
  })
  cy.get(`${inputTag}`).then(function (el) {
    for(const prop in this) {
      if (prop.includes("file")) {
        arr.push(this[prop])
      }
    }
    const list = new DataTransfer()
  
    dataJson.files.forEach((item, i) => {
      // convert the logo base64 string to a blob
      const blob = Cypress.Blob.base64StringToBlob(arr[i], mineType)
  
      const file = new FileCopy([blob], `${item}`, { type: mineType }, `${ dirName + item }`)
      const pathName = dirName.slice(1)
      file.webkitRelativePath = `${ pathName + item}`
      console.log(file)
      list.items.add(file)
    })
  
    const myFileList = list.files
    
    el[0].files = myFileList
    el[0].dispatchEvent(new Event('change', { bubbles: true }))
  })

})

The usage:

First, prepare a data.json file inside the fixtures folder, example:

data.json
{
  "files":[
    "1_TEST-JOHN-01.jpeg",
    "2_TEST-JOHN-01.jpeg",
    "3_TEST-JOHN-01.jpeg",
    "4_TEST-JOHN-01.jpeg",
    "5_TEST-JOHN-01.jpeg",
    "6_TEST-JOHN-01.jpeg",
    "7_TEST-JOHN-01.jpeg",
    "8_TEST-JOHN-01.jpeg",
    "9_TEST-JOHN-01.jpeg",
    "10_TEST-JOHN-01.jpeg"
  ]
}

Second, import the json data into your spec.js

import data from '../fixtures/data.json'

Third, Write a class to extend the File web API object with the functions to set and get webkitRelativePath value

class FileCopy extends File {
  constructor(bits, filename, options) {
    super(bits, filename, options)
    let webkitRelativePath
    Object.defineProperties(this, {

        webkitRelativePath : {

            enumerable : true,
            set : function(value){
                webkitRelativePath = value;
            },
            get : function(){
                return webkitRelativePath;
            } 
        },
    });
  }

}

Finally, call the cmd in the spec.js

cy.uploadMultiFiles(
      {
        dataJson:data, // the data.json you imported.
        dirName:"/your/dirname/",
        inputTag:"input#upload",
        mineType:"image/jpeg"
      }
    )
John
  • 31
  • 5
0

if your file input display: none; use

cy.get('[data-type=inputFile]').selectFile('cypress/fixtures/avatar.jpg', { force: true })

else

 cy.get('[data-type=inputFile]').selectFile('cypress/fixtures/avatar.jpg')