Felix PK
Felix PK

Reputation: 154

Upload File with Cypress.io via Request

Given this simple form

<form action="/upload_file" method="POST" enctype="multipart/form-data" id="upload_file_form">
    <input type="file" name="file" required>
    <input type="text" placeholder="Select file" disabled>
    <input type="submit" value="upload">
</form>

Since native events are not supported by cypress.io (can't select a file), I have to use a post-request.

The request structure in cypress.io looks like

cy.request({
    method: 'POST',
    url: '/some/url',
    form: true,
    body: { ... },
    headers:{ ... }
})

I would like to know how I could send a simple *.txt

Any suggestions appreciated!

Upvotes: 10

Views: 20275

Answers (6)

Qarj
Qarj

Reputation: 21

I spent most of the weekend on this so will post the solution that works for me.

I couldn't get the example in the NPM package cypress-upload-file-post-form to work as it is written. Specifically I got a function not found error on Cypress.Blob.binaryStringToBlob.

Step 1

In support/commands.js add this function

Cypress.Commands.add('multipartFormRequest', (method, url, formData, done) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function () {
        done(xhr);
    };
    xhr.onerror = function () {
        done(xhr);
    };
    xhr.send(formData);
});

Step 2

Add a file called Base64TestCV.rtf to the fixtures folder.

I wanted a .rtf file but obviously you can make a .txt file - whatever you do it must be encoded in base64 by using an online converter or the base64 command in Linux.

For testing purposes, make sure it contains the text MyCypressTestCV (encoded in base64 obviously).

Step 3

Create and run your test. In this example my .rtf file contains the text MyCypressTestCV which is reflected back by httpbin.org so it proves it was decoded from base64 correctly and uploaded.

describe('Upload a file to a multipart form using cy.request', function () {
    it('Performs a multipart post to httpbin.org', function () {
        const baseUrl = 'http://httpbin.org';
        const postUrl = `${baseUrl}/post`;

        cy.visit(baseUrl); // Cypress will be very buggy if you don't do at least one cy.visit

        cy.request(baseUrl).as('multipartForm'); // pretend we are doing the GET request for the multipart form

        const base64FileName = 'Base64TestCV.rtf';
        const postedFileName = 'TestCV.rtf';
        const method = 'POST';
        const mimeType = 'application/rtf';

        cy.fixture(base64FileName).as('base64File'); // file content in base64

        cy.get('@multipartForm').then((response) => {
            const formData = new FormData();
            formData.append('firstFormField', 'Hello');
            formData.append('secondFormField', '25');
            const blob = Cypress.Blob.base64StringToBlob(this.base64File, mimeType);
            formData.append('uploadFile', blob, postedFileName);

            cy.multipartFormRequest(method, postUrl, formData, function (response) {
                expect(response.status).to.eq(200);
                expect(response.response).to.match(/MyCypressTestCV/); // http://httpbin.org/post reflects what you post
            });
        });
    });
});

Upvotes: 0

maxime1992
maxime1992

Reputation: 23793

I just needed from Cypress context to upload a file somewhere (to mock something out before the browser does request it).

The following is working just fine in my case:

cy.fixture('something.svg').then((file) => {
  cy.request({
    url: 'http://mock/uploads/some-id',
    method: 'put',
    body: file,
  });
});

Upvotes: 2

abramenal
abramenal

Reputation: 1

While waiting for the built-in support, we can stop copy-pasting the hack and create a reusable package out of it. Feel free to open issues so we can make it suitable for the most popular use cases.

Note that's more of just uploading a file and not focused on a form specifically.

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

Upvotes: -3

Brendan
Brendan

Reputation: 4649

I ran into this issue today and developed a similar solution to the other answers, but it allows you to use the .then() Cypress syntax.

Add this to support/commands.js:

Cypress.Commands.add("form_request", (url, formData) => {
  return cy
    .server()
    .route("POST", url)
    .as("formRequest")
    .window()
    .then(win => {
      var xhr = new win.XMLHttpRequest();
      xhr.open(method, url);
      xhr.send(formData);
    })
    .wait("@formRequest");
  });

Then you'd be able to do the following in your tests:

cy
  .form_request(url, formData)
  .then(response => {
    // do stuff with your response
  });

Upvotes: 1

Javier Aviles
Javier Aviles

Reputation: 10665

Will leave this here in case it helps somebody https://github.com/javieraviles/cypress-upload-file-post-form

Second scenario (send_form_data_with_file_in_post_request_spec.js):

I want to build up the FormData myself( new FormData(), formData.append/formData.set ) and send it directly with a POST request to the backend or submit the form with the FormData I have created. For this case, the transmitted data must be in the same format as the form's submit(), which type is set to "multipart/form-data". Having a look at the MDN web docs to see how you can build a FormData: Using FormData Objects, and knowing that at this very moment (Cypress 2.1.0) cy.request doesn't support FormData (multipart/form-data) so we will need a XMLHttpRequest, the test can be performed as follows. Include the following code in your "commands.js" file within the cypress support folder, so the command cy.form_request() can be used from any test:
// Performs an XMLHttpRequest instead of a cy.request (able to send data as 
// FormData - multipart/form-data)
Cypress.Commands.add('form_request', (method, url, formData, done) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function () {
        done(xhr);
    };
    xhr.onerror = function () {
        done(xhr);
    };
    xhr.send(formData);
})

Then, in case you want to send the same form as before (form containing an excel file and other plain inputs) but build it by yourself and send it directly to the server, the test would be something like this:

describe('Testing the API', function () {

    it('Receives valid FormData and proccesses the information correctly', 
        function () {

    /*
    The reason why this test may look a bit tricky is because the backend endpoint is expecting the 
    submission of a web Form (multipart/form-data), not just data within a POST. The "cy.request()" 
    command doesn't support sending a web Form as a body in a POST request, so the test uses a support 
    command that has been created to perform a genuine XMLHttpRequest where a web Form can be placed.
    */

    //Declarations
    const fileName = 'your_excel_file.xlsx';
    const method = 'POST';
    const url = 'http://localhost:3000/api/excel_form';
    const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
    const inputContent2 = 'input_content2';
    const expectedAnswer = '{"msg":"X elements from the excel where successfully imported"}';

    // Get file from fixtures as binary
    cy.fixture(fileName, 'binary').then( (excelBin) => {

        // File in binary format gets converted to blob so it can be sent as Form data
        Cypress.Blob.binaryStringToBlob(excelBin, fileType).then((blob) => {

            // Build up the form
            const formData = new FormData();
            formData.set('file', blob, fileName); //adding a file to the form
            formData.set('input2', inputContent2); //adding a plain input to the form
            .
            .
            .
            // Perform the request
            cy.form_request(method, url, formData, function (response) {
                expect(response.status).to.eq(200);
                expect(expectedAnswer).to.eq(response.response);
            });

        })

    })

})

})

Upvotes: 8

rcambrj
rcambrj

Reputation: 602

bahmutov's cypress-form-data-with-file-upload makes reference to the github issues (311 and 170) where this problem is being considered and provides a rather dirty workaround for those that cannot accommodate Cypress' limitation on this front.

The workaround is to create a custom XHR object and use that to submit the request with the file attached, instead of Cypress' cy.request. The provided workaround is limited to functioning with an <input type="file" /> element's value. In my case, I needed to use a fixture in my request, so my solution was to create a custom cypress command

Cypress.Commands.add("xhr", (method, url, formdata) => {
    // inspired by https://github.com/bahmutov/cypress-form-data-with-file-upload/blob/8fe6106d28eef0634c78564c746746d1cc354e99/index.js
    // fixes lack of FormData (multipart/form-data) support in cy.request
    cy.window()
    .then(win => {
        return new Promise((resolve, reject) => {
            const XHR = new win.XMLHttpRequest()
            XHR.onload = resolve
            XHR.open(method, url)
            XHR.send(formdata)
        })
    })
})

And attach Blobs or Files as needed to an instance of FormData.

I strongly recommend accommodating Cypress, for example, by accepting base64-encoded file data as plain text, if you can, due to the complexity associated with this workaround.

Upvotes: 1

Related Questions