Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Ability to Submit File to Input Element From Local Filesystem (file upload) #170

Open
pihish opened this issue Jun 15, 2016 · 173 comments
Open

Comments

@pihish
Copy link

@pihish pihish commented Jun 15, 2016

Description
Our app takes a file from an user, does some work with it, and produces outputs based on the input the user provides. A bunch of things happen in the system during the processing and we expect project pages to be generated in a certain fashion after a project successfully completes. We need to be able to submit files through our webpage to test the affects of this process.

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Jun 16, 2016

This requires sending native events.

We've been holding off on adding native event support because of the way the debugger protocol works in Chrome. It can only accept one connection which means dev tools cannot be open. Forcing users to close their dev tools is a big reason why the testing ecosystem is so awful.

We've been investigating the user experience around this and we have some pretty good ideas about how to improve this. Once we add native events you'll be able to not just upload files but send native keyboard events, mobile events, and a slew of other things.

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Jan 18, 2017

Should also note that it is possible to upload files in your application but its different based on how you've written your own upload code.

For instance if you use the new fileReader API's you could use cy.readFile or cy.fixture to read in data, and then convert it to a Blob and then trigger the specific event your application is looking for and send in the Blob as a property.

Because the implementation is different per application there is no way for us to suggest how to do it. Once we implement native API's we will be able to show a recipe handling this.

@wescarr
Copy link

@wescarr wescarr commented Jun 26, 2017

Wanted to add that I got this working by doing the following:

cy.fixture('path/to/image.png').as('logo')
  .get('input[type=file]').then(function(el) {
    return Cypress.Blob.base64StringToBlob(this.logo, 'image/png')
      .then(blob => {
        el[0].files[0] = blob
        el[0].dispatchEvent(new Event('change', {bubbles: true}))
      })
  })
@medeeiros
Copy link

@medeeiros medeeiros commented Jul 13, 2017

I've tried everything and I can't get dropzone on my react app to recognise the file.

cy.get('input[type=file]', { force: true }).then(function(el) {
    const xml = new Blob([this.xbrl], { type: 'text/xml' } )

    el[0].files[0] = xml
    el[0].dispatchEvent(new Event('change', {
        bubbles: true,
        target: {
            files: [xml],
        }
    }))
})
@dziamid
Copy link

@dziamid dziamid commented Aug 17, 2017

@brian-mann

Once we add native events you'll be able to not just upload files but send native keyboard events, mobile events, and a slew of other things.

Is there any progress on the road to native events? Having to hack around for file uploading, hovering and tabbing is a big deal for us.

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Aug 17, 2017

Chromium team is still working on multiplex'd debugger support. Implementing native events without this is significantly difficult and there are many edge cases and UI changes we'd have to account for, and then once they do land this, we'd have to fork the code and handle it in a separate way.

I'm still on the side of waiting until this is implemented properly by them. You can essentially achieve most things without native event by firing events directly. You can't do things like tab, but you could always use a different tool just for those tests, while letting Cypress handle all the rest.

You can test file uploads using events directly if your app is using HTML5 API's and its not like an oldschool traditional form.

@dziamid
Copy link

@dziamid dziamid commented Aug 17, 2017

Thanks for the detailed response. While I could get away without tabbing and work around file upload, this involved a lot of boilerplate, complexity and changes in the application code. Nevertheless, great product anyway and good to know that you are looking forward to switching to native events when things get stable on chrome side!

@rdamborsky
Copy link

@rdamborsky rdamborsky commented Oct 19, 2017

Using el[0].files[0] = blob as @wescarr recommends worked until few weeks back (I don't recall what Chrome version it was).

But now, at least in v.61+ this no longer works and errors out with "TypeError: Failed to set an indexed property on 'FileList': Index property setter is not supported."

This is not an issue on side of Cypress. I just felt mentioning it might save time to someone running into this thread. Unfortunately, I didn't find a way around this yet.

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Oct 19, 2017

Haven't looked into this, but oftentimes you can overwrite things manually using Object.defineProperty which can forcibly change the descriptor for values.

That is of course if it's configurable: true

@robrkerr
Copy link

@robrkerr robrkerr commented Oct 23, 2017

I'm having the same issue @rdamborsky mentioned and can't find a way around it.

@aboutlo
Copy link

@aboutlo aboutlo commented Oct 27, 2017

@brian-mann I tried to overwrite but it seems not possible:
https://stackoverflow.com/questions/1711357/how-would-you-overload-the-operator-in-javascript

The FileList descriptor is empty
Object.getOwnPropertyDescriptors($input[0].files) // => {}

Any suggestion to get a file upload tested? Perhaps we could pass an option to the cypress run command to turn off chrome security sandbox?

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Oct 27, 2017

As mentioned above - there is no way to set values in file upload inputs, and there are no flags that chrome exposes to force this. You have to use native events. I commented on this recently yesterday.

#311 (comment)

The only other way to get file uploads working today is to understand how your application handles them with File API and then stub it out. It's possible but not generic enough to give any advice on it.

@aboutlo
Copy link

@aboutlo aboutlo commented Oct 30, 2017

tks @brian-mann 👍

@rafiek
Copy link

@rafiek rafiek commented Nov 16, 2017

I think Cypress is really awesome and am already using it within my company. We previously used Protractor and were able to add a file to an input[type="file"] element. Would really love to be able to this as well with Cypress, because it is sometimes an essential step when doing full e2e tests for our applications.

@anned20
Copy link

@anned20 anned20 commented Nov 16, 2017

Hey everyone!

I fixed this for DropZone using this piece of code:

const dropEvent = {
    dataTransfer: {
        files: [
        ],
    },
};

cy.fixture('Path to picture fixture').then((picture) => {
    return Cypress.Blob.base64StringToBlob(picture, 'image/jpeg').then((blob) => {
        dropEvent.dataTransfer.files.push(blob);
    });
});

cy.get('Your dropzone element').trigger('drop', dropEvent);

I hope this will work for you too!

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Nov 16, 2017

@anned20 this is really good - thank you for this.

One tidbit - you're missing a return statement on Cypress.blob.base64StringToBlob because that returns a promise and the cypress cy.then needs to know about it else it will not properly await it. This is working in your example by chance because the blob promise is resolving faster than the later cy.get and cy.trigger resolve.

Return the promise will always finish the promise chain first before moving on.

@anned20
Copy link

@anned20 anned20 commented Nov 16, 2017

@brian-mann I updated the example, is this what you meant?

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Nov 16, 2017

Yup 👍

@vicusbass
Copy link

@vicusbass vicusbass commented Nov 23, 2017

Is there any workaround (other than using Electron) for file inputs (the dropzone example is not working for me) or we have to wait for the native events implementation?

@brian-mann
Copy link
Member

@brian-mann brian-mann commented Nov 23, 2017

The workaround is however your application is built. You fire the events and provide the object value properties and/or methods that you application uses to respond to the upload events.

There is no generic solution - you have to understand how your application works. Then however it works you fire what it needs to respond.

@danceric0919
Copy link

@danceric0919 danceric0919 commented Nov 24, 2017

I'm new to Cypress.io and encounter issue when testing upload file implemented by Vue.js
We have a component like following:

<b-form-file id="upload-file" v-model="files[0]" @change="upload(payload)" >
</b-form-file>

I tried
cy.get('#upload-file').trigger('change', somepayload)
but nothing happened

is there any way to trigger change or proper event for application implemented by Vue?

Thanks for any respond.
Cheers.

@bahmutov
Copy link
Contributor

@bahmutov bahmutov commented Nov 27, 2017

@danceric0919 can you just call upload() method on the Vue component around this element? Like load the context from Cypress fixture then call the method? I should maybe make a recipe for this that uses Vue.js - where is b-form-file coming from? Is it a public component?

Ok found it - https://bootstrap-vue.js.org/docs/components/form-file/

@bahmutov
Copy link
Contributor

@bahmutov bahmutov commented Nov 27, 2017

@danceric0919 can you take a look how I test file upload in bahmutov/vue-vuex-todomvc@deff47a ? Just create a File in your test, set it in the component and trigger change event.

@danceric0919
Copy link

@danceric0919 danceric0919 commented Nov 28, 2017

@bahmutov thanks for the reply.
I'll try it later today or tomorrow.
Appreciate 👍

2017/11/29 update
After updating application slightly, your example works in my site.
Really appreciate for the example, it saved my days.

@epaaj
Copy link

@epaaj epaaj commented Apr 27, 2020

Thanks to @guillem-catala and @mattcarlotta for providing a solution.

I had to change cy.fixture(fileName) into cy.fixture(fileName, 'base64) in order to get .xlsx files to upload. This is the only solution I have found that is working. Tried a few others mentioned here, and 'cypress-file-upload' package that didn't work at all.

In the test:

cy.get('[name="topic[photo]"]')
  .attach_file('files/forum-attachment.jpg', 'image/jpg')
  .trigger('change', { force: true });

as custom cypress command in commands.js:

Cypress.Commands.add(
  'attach_file',
  {
    prevSubject: 'element',
  },
  (input, fileName, fileType) => {
    cy.fixture(fileName, 'base64')
    .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;
    })
  }
)
@x-yuri
Copy link

@x-yuri x-yuri commented May 3, 2020

@epaaj I have an idea why cypress-file-upload@3.x didn't work for you with *.xlsx files. The thing is that cy.fixture() handles images specially. It reads them as base64. And so does cypress-file-upload. It falls back to base64 if encoding wasn't passed.

But in case of Excel files, cy.fixture() defaults to utf8, which is not an option. You can't read a binary as utf8 and expect a result. So, first you've got to pass the desired encoding to cy.fixture(). Next, cypress-file-upload has no defaults for every possible extension, and even if it had, the encodings must match (passed to cy.fixture() and to cypress-file-upload).

it('works', () => {
    const fileName = 'spreadsheet.xlsx';
    const encoding = 'base64';
    cy.visit('/');
    cy.fixture(fileName, encoding).then(fileContent => {
        cy.get('input[type="file"]')
            .upload({fileContent, fileName, mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', encoding});
    });
    cy.get('input[type="submit"]').click();
});

Can you please check? I think it'd be of great help to people here.

@rodybothebynder
Copy link

@rodybothebynder rodybothebynder commented May 7, 2020

@guillem-catala

Is it also possible to upload an array of items? Two or more?

@rodybothebynder
Copy link

@rodybothebynder rodybothebynder commented May 14, 2020

uploadAllFiles(nameJPG, namePNG, namePSD, nameTIF, nameMP4, nameDOC) {
            cy.fixture('tif.tif').then(tif => {
            cy.fixture('mp4.mp4').then(mp4 => {
            cy.fixture('png.png').then(png => {
            cy.fixture('jpg.jpg').then(jpg => {
            cy.fixture('docx.docx', 'base64').then(doc => {
            cy.fixture('psd.psd', 'base64').then(psd =>  {
                const files = [
                    { fileName: `${namePNG}.png`, fileContent: png, mimeType: 'image/png' },
                    { fileName: `${nameJPG}.jpg`, fileContent: jpg, mimeType: 'image/jpg' },
                    { fileName: `${nameTIF}.tif`, fileContent: tif, mimeType: 'image/tiff' },
                    { fileName: `${nameMP4}.mp4`, fileContent: mp4, encoding: 'utf8', mimeType: 'video/mp4' },
                    { fileName: `${namePSD}.psd`, fileContent: psd, encoding: 'base64', mimeType: "image/vnd.adobe.photoshop"},
                    { fileName: `${nameDOC}.docx`, fileContent: doc, encoding: 'base64', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}
                ];
            cy.get('input[type="file"]').upload(files);
            cy.get('.notification h1', {timeout: 60000}).should('be.visible');
               })
             });
            });
          });
        });
      });
@narinepoghosyan
Copy link

@narinepoghosyan narinepoghosyan commented Jun 16, 2020

Any news from this issue. File upload has not working after chrome 70 + versions

@IliasLG
Copy link

@IliasLG IliasLG commented Jun 24, 2020

Any news from this issue. File upload has not working after chrome 70 + versions

Just tried the solution @guillem-catala provided. Works flawlessly on images (tested png and jpg)

@cbreddy11
Copy link

@cbreddy11 cbreddy11 commented Jun 30, 2020

Any help please. I have to upload a image on clicking a button

@crisgrim
Copy link

@crisgrim crisgrim commented Aug 6, 2020

Until Cypress has a good implementation about it, this library works for me.

cypress-file-upload

@heitrix
Copy link

@heitrix heitrix commented Aug 20, 2020

Hello @guillem-catala @epaaj ,

With the changes on blob in cypress 5 https://docs.cypress.io/guides/references/changelog.html#5-0-0 , will there any code changes on your solution? I'm a bit worried to update to cypress 5 if my test will break? It's working well on Cypress 4.12.1 by the way.

Thank you,

@heitrix
Copy link

@heitrix heitrix commented Aug 21, 2020

Nevermind, here's what works for me on Cypress 5

Cypress.Commands.add(
  "attach_img",
  {
    prevSubject: "element",
  },
  (input, fileName, fileType) => {
    cy.fixture(fileName).then((content) => {
      const blob = Cypress.Blob.base64StringToBlob(content, fileType);
      const testFile = new File([blob], fileName);
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(testFile);
      input[0].files = dataTransfer.files;
      return input;
    });
  }
);

Apology, I'm new at this. Don't even know to make the code shows properly with line break here.

@jennifer-shehane
Copy link
Member

@jennifer-shehane jennifer-shehane commented Aug 21, 2020

In 5.0.0, Cypress updated the Blob utility from 1.3.3 to 2.0.2, which itself had a broken change underneath.

The return type of the Cypress.Blob methods arrayBufferToBlob, base64StringToBlob, binaryStringToBlob, and dataURLToBlob have changed from Promise to Blob.

You will need to be update your code which uses Cypress.Blob to reflect this change outlined in our migration guide: https://on.cypress.io/migration-guide#Return-type-of-Cypress-Blob-changed

The cypress-file-upload plugin is currently not working in 5.0 as outlined here: abramenal/cypress-file-upload#214 They have a PR open to fix the issue: abramenal/cypress-file-upload#215, so you may want to hold off on updating to 5.0.0 if you use that plugin until that fix is merged.

The cypress-file-upload plugin was fixed in its latest release: https://github.com/abramenal/cypress-file-upload/releases/tag/v4.1.0

@lorainegarutti
Copy link

@lorainegarutti lorainegarutti commented Aug 27, 2020

I'm sorry if it's a stupid question... I didn't get what I should set for 'input' parameter, if someone can help me!

Thanks!

Nevermind, here's what works for me on Cypress 5

Cypress.Commands.add(
  "attach_img",
  {
    prevSubject: "element",
  },
  (input, fileName, fileType) => {
    cy.fixture(fileName).then((content) => {
      const blob = Cypress.Blob.base64StringToBlob(content, fileType);
      const testFile = new File([blob], fileName);
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(testFile);
      input[0].files = dataTransfer.files;
      return input;
    });
  }
);

Apology, I'm new at this. Don't even know to make the code shows properly with line break here.

@jennifer-shehane jennifer-shehane changed the title Adding Ability to Submit File to Input Element From Local Filesystem Adding Ability to Submit File to Input Element From Local Filesystem (file upload) Aug 28, 2020
@jennifer-shehane
Copy link
Member

@jennifer-shehane jennifer-shehane commented Aug 28, 2020

The cypress-file-upload plugin has fixed the compatibility issue with Cypress 5.0 in its latest release: https://github.com/abramenal/cypress-file-upload/releases/tag/v4.1.0

@86SUSHIL
Copy link

@86SUSHIL 86SUSHIL commented Oct 4, 2020

I used a similar workaround, and I found that when reading in the file contents they were coming in as string data instead of binary data. To fix this, I found a function online (can't find the original source at the moment) and modified it…

Here it is (Typescript):

/**
 * Decode base64 data to a binary File object
 * @param  b64Data     The base-64 encoded data
 * @param  filename    The filename for the file
 * @param  contentType The content type for the fle
 * @param  sliceSize   The size for the byte arrays
 * @return             The resulting file object
 */
function b64toFile(b64Data, filename: string, contentType: string = '', sliceSize: number = 512): File {
    let byteCharacters = atob(b64Data);
    let byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        let slice = byteCharacters.slice(offset, offset + sliceSize);

        let byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }

        let byteArray = new Uint8Array(byteNumbers);

        byteArrays.push(byteArray);
    }

    let file = <any>(new Blob(byteArrays, {
        type: contentType
    }));

    // give the blob object the missing data members for a file
    file.lastModifiedDate = new Date();
    file.name = filename;

    return <File>file;
}

Then do something like this:

        var fileContents;
        cy.fixture('file.pdf', 'base64').then(file => {
            fileContents = b64toFile(file, 'file.pdf', 'application/pdf');
        });

        cy.get('#my-file-input').then($fileInput => {
            const fileData = new ClipboardEvent('').clipboardData || new DataTransfer();
            fileData.items.add(new File([fileContents], 'resume.pdf', {type: 'application/pdf'}));
            (<any>$fileInput.get(0)).files = fileData.files;
        });

can you please let me know this code in javascript plesae

@heitrix
Copy link

@heitrix heitrix commented Oct 5, 2020

I used a similar workaround, and I found that when reading in the file contents they were coming in as string data instead of binary data. To fix this, I found a function online (can't find the original source at the moment) and modified it…
Here it is (Typescript):

/**
 * Decode base64 data to a binary File object
 * @param  b64Data     The base-64 encoded data
 * @param  filename    The filename for the file
 * @param  contentType The content type for the fle
 * @param  sliceSize   The size for the byte arrays
 * @return             The resulting file object
 */
function b64toFile(b64Data, filename: string, contentType: string = '', sliceSize: number = 512): File {
    let byteCharacters = atob(b64Data);
    let byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        let slice = byteCharacters.slice(offset, offset + sliceSize);

        let byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }

        let byteArray = new Uint8Array(byteNumbers);

        byteArrays.push(byteArray);
    }

    let file = <any>(new Blob(byteArrays, {
        type: contentType
    }));

    // give the blob object the missing data members for a file
    file.lastModifiedDate = new Date();
    file.name = filename;

    return <File>file;
}

Then do something like this:

        var fileContents;
        cy.fixture('file.pdf', 'base64').then(file => {
            fileContents = b64toFile(file, 'file.pdf', 'application/pdf');
        });

        cy.get('#my-file-input').then($fileInput => {
            const fileData = new ClipboardEvent('').clipboardData || new DataTransfer();
            fileData.items.add(new File([fileContents], 'resume.pdf', {type: 'application/pdf'}));
            (<any>$fileInput.get(0)).files = fileData.files;
        });

can you please let me know this code in javascript plesae

I think you can use the plugin now since they've fixed them.

@zc9
Copy link

@zc9 zc9 commented Nov 4, 2020

I think you can use the plugin now since they've fixed them.

@paulinetheitgirl
Copy link

@paulinetheitgirl paulinetheitgirl commented Dec 10, 2020

I'm sorry if it's a stupid question... I didn't get what I should set for 'input' parameter, if someone can help me!

Thanks!

Nevermind, here's what works for me on Cypress 5

Cypress.Commands.add(
  "attach_img",
  {
    prevSubject: "element",
  },
  (input, fileName, fileType) => {
    cy.fixture(fileName).then((content) => {
      const blob = Cypress.Blob.base64StringToBlob(content, fileType);
      const testFile = new File([blob], fileName);
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(testFile);
      input[0].files = dataTransfer.files;
      return input;
    });
  }
);

Apology, I'm new at this. Don't even know to make the code shows properly with line break here.

@lorainegarutti not sure if you figured out out already, but the way is to chain from a .get() . See @epaaj 's comment from 27 April: #170 (comment) in this thread.

@debbyzang
Copy link

@debbyzang debbyzang commented Dec 30, 2020

what should I do, if I want to upload a file, when the type=button and this button on a pop-up ?

@jimpriest
Copy link

@jimpriest jimpriest commented Feb 16, 2021

Trying to upload a PDF file. I've tried cypress-file-upload plugin, and other solutions mentioned here in #170.

I'm not sure if it's Cypress or my application (an older ColdFusion application) but regardless of what I try I get this error:

InvalidStateError
Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.

My file seems like it's uploaded - my form continues on to the next page ...
cypress-file-upload1

Post form upload - but Cypress stops since there was an error... :
cypress-file-upload2

I can also open the browser console - click on that step in the test and see that the input value is set correctly?

<input type="file" id="fileupload" name="uploadFile" accept=".pdf" style="width: 400px;" value="test-pdf.pdf">

Test code to reproduce

This application is not available on Internet and due to it be a dynamic application I can't easily share.

Versions

Cypress 6.3.0
cypress-file-upload 5.0.2

While I love Cypress I'm struggling to do anything file related. :(

Update
This is an ugly hack but allows my upload and test to complete:

cy.get("#fileupload").attachFile('test-pdf.pdf', );

// cypress for whatever reason throws an error when 
// when you click save to submit the form - we'll catch and suppress 
// that error so the test will continue

cy.on('uncaught:exception', (err, runnable) => {
    expect(err.message).to.include('Failed to set')
    done()
    return false
  })

cy.get("#save").click();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet