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

Drag & Drop upload #669

Closed
SMenigat opened this issue Sep 19, 2017 · 40 comments
Closed

Drag & Drop upload #669

SMenigat opened this issue Sep 19, 2017 · 40 comments
Labels
pkg/driver This is due to an issue in the packages/driver directory type: question

Comments

@SMenigat
Copy link

SMenigat commented Sep 19, 2017

We are still looking for an option to test Drag & Drop file uploads.
Is there anything in the plans about this?

Would be nice if I could drop a fixture onto the selected subject. Something like this maybe:

// fixture reference according to: 
// https://docs.cypress.io/api/commands/fixture.html#Shortcuts
cy.get('.dnd-upload-area').dropFile('fixture:image.jpg');

Thanks in advance 😊

@jennifer-shehane
Copy link
Member

We actually have a way to do this in version 0.20.0. We have our own drag-and-drop folder select in our Desktop App if you globally install cypress that we test using Cypress. Just going to throw in some of our tests for this (which will soon be Open Source). This is a bit of a firehose, but hopefully it's helpful.

Mostly you're looking to use the new .trigger() command.

describe('Drag and Drop', function() {
  beforeEach(function() {
    this.dropEvent = {
      dataTransfer: {
        files: [{ path: '/foo/bar' }]
      }
    }
  })

  it('shows project drop area with button to select project', function() {
    cy.get('.project-drop p:first')
      .should('contain', 'Drag your project here')
  })

  describe('dragging and dropping project', function() {
    it('highlights/unhighlights drop area when dragging over it/leaving it', function() {
      cy.get('.project-drop')
        .trigger('dragover')
        .should('have.class', 'is-dragging-over')
        .trigger('dragleave')
        .should('not.have.class', 'is-dragging-over')
    })

    it('unhighlights drop area when dropping a project on it', function() {
      cy.get('.project-drop')
        .trigger('dragover')
        .should('have.class', 'is-dragging-over')
        .trigger('drop', this.dropEvent)
        .should('not.have.class', 'is-dragging-over')
    })

    it('adds project and opens it when dropped', function() {
      cy.get('.project-drop')
        .trigger('drop', this.dropEvent)
    })
  })
})

@jennifer-shehane jennifer-shehane added the pkg/driver This is due to an issue in the packages/driver directory label Sep 20, 2017
@SMenigat
Copy link
Author

Wow thanks a lot @jennifer-shehane ! I'm going to try this out tomorrow and give you feedback about my success here.

@jennifer-shehane
Copy link
Member

Hi @SMenigat, did this work out for you?

@egucciar
Copy link
Contributor

egucciar commented Nov 8, 2017

Could also use some guidance on this approach, turns out I decide to start doing TDD when I'm building an upload screen w/ drag and drop existing third party
EDIT: I'll add I did try it but it didn't work, so I tested the other functionality in the form and might have to circle back to upload in a day or two

@jennifer-shehane
Copy link
Member

Hey @egucciar, what about the test code above did not work exactly? Did you get an error? Did the assertions fail?

I will add that some of the test code I pasted is particular to our implementation, so you will have to be familiar with how your drag and drop area is coded.

Our implementation is open source here: https://github.com/cypress-io/cypress/blob/develop/packages/desktop-gui/src/app/intro.jsx#L30

With the tests for this implementation here: https://github.com/cypress-io/cypress/blob/develop/packages/desktop-gui/cypress/integration/global_mode_spec.coffee#L75

@egucciar
Copy link
Contributor

egucciar commented Nov 8, 2017

I guess the issue might be that I'm using angular-file-upload? Could that be it?

@jennifer-shehane

@jennifer-shehane
Copy link
Member

@egucciar You have to look at the way the particular drag and drop you want to test is currently working. The way I do this is, within the Sources panel of the DevTools, you set some Event Listener Breakpoints for Drag/drop, then you use the drag and drop normally in your app by dragging an image in. Then you can see exactly what these events look like and the data that it uses in your application.

Set a global Event Listener Breakpoint
screen shot 2017-11-08 at 3 01 35 pm

The dataTransfer object from me dragging in a photo manually to their site
screen shot 2017-11-08 at 3 04 31 pm

Doing this with the demo page of angular-file-upload, I can see that a dragover, drop and dragleave are all triggered (you'll need to select and deselect them one by one as you set the breakpoint to inspect each one.

From this, I can see that they have code around more data than my implementation above cared about in their dragover and drop event. They're expecting a types array in the dataTransfer object for example. And in order to get the filename to show up in the table, they have more data in their files array. The test code below should help get you started. I'm closing this issue.

describe('Drag and Drop', function() {
  const fileName = "foobar.jpg"
  
  beforeEach(function() {
    this.dropEvent = {
      dataTransfer: {
        files: [{
          name: fileName,
          size: 32796,
          type: "image/jpeg",
          lastModified: 1510154936000,
          webkitRelativePath: ""
        }],
        types: ["Files"]
      }
    }
  })
  
  it('drag and drop upload', function(){
    cy.visit('http://nervgh.github.io/pages/angular-file-upload/examples/simple/')
    cy.get('.my-drop-zone').first()
      .trigger('dragover', this.dropEvent)
      .should('have.class', 'nv-file-over')
      .trigger('drop', this.dropEvent)

    cy.get('table').contains(fileName)
  })
})

@egucciar
Copy link
Contributor

egucciar commented Nov 8, 2017

@jennifer-shehane thats perfect and it works. Thanks so much for explaining your methodology as it will come in handy when we implement upload tests for other non angular code bases!!!

@EirikBirkeland
Copy link
Contributor

EirikBirkeland commented Mar 28, 2018

@jennifer-shehane Thanks for the extremely helpful debugging post above. I learned a lot!

I tried this, and I was able to get the drag-n-drop itself working, but the resulting POST has completely incorrect content somehow, and the server returns 500.

It may relate to my getAsFile method mock below (I looked at the HTML5 spec and just tried some stuff):

// Drag and drop a file to upload it
const dropEvent = {
    dataTransfer: {
        dropEffect: "none",
        effectAllowed: "all",
        files: [],
        items: [
            {
                kind: "file",
                type: "text/plain",
                // TODO: Mock getAsFile: https://www.w3.org/TR/html51/editing.html#a-drag-data-item-kind
                getAsFile: function() {
                    return dropEvent.dataTransfer.files.length
                        ? dropEvent.dataTransfer.files[0]
                        : null;
                }
            }
        ],
        types: ["Files"]
    }
};

cy.get('#upload-files').trigger('dragover', dropEvent)
    .should('have.class', 'dragover')
    .then(() => {
    dropEvent.dataTransfer.files.push({
        path: "/tmp/upload_cache/9a4611785ac01f967598d59ab536ee/2824-document.txt",
        name: "2824-document.txt",
        lastModified: 1522144523092,
        size: 36,
        type: "text/plain",
        webkitRelativePath: ""
    })
}).trigger('drop', dropEvent);

@EirikBirkeland
Copy link
Contributor

Or perhaps I am misunderstanding here: I was expecting to upload a file from the harddrive.

Perhaps I should restrict the scope of the test to actually testing the visuals drag'n'drop, without expecting end-to-end functionality?

@blaynem
Copy link

blaynem commented Mar 30, 2018

@jennifer-shehane thank you so much, this helped me a ton! Was a little confused at first, but ended up working through it.

For our specific drag and drop, it didn't care about mousedown, mouseup or any sort of clicking. After digging a bit deeper into the code, found that I had to call dragstart, dragenter, and drop. Drag start began on the element I wanted to drag, then I needed to dragenter on the element that it needed to be dropped at. And finally, had to grab the element we wanted to drag one more time and drop it there.

Been working on this all day, so pretty hyped right now.

Example Code:

describe('a user can change the order of specific action types', () => {
    const { firstChip, secondChip, thirdChip } = rulesPageConstants
    it.only('can reorder boost actions', () => {
      setRuleActions(0)
      cy
        .get('#boosts-input-icon').click()
        .get('#reorder-actions-dnd-chip-0').as('firstChip')
          .should('have.text', firstChip)
        .get('#reorder-actions-dnd-chip-1').as('secondChip')
          .should('have.text', secondChip)
        .get('#reorder-actions-dnd-chip-2').as('thirdChip')
          .should('have.text', thirdChip)
        .get('@firstChip')
          .trigger("dragstart")
        .get('@secondChip')
          .trigger('dragenter')
        .get('@firstChip')
          .trigger('drop')
    })
  })

@kutlaykural
Copy link

@jennifer-shehane
Hi!
Same code is not work for me. Trigger with "drop" not gives any reaction at dropable area.

dataTransfer object is little different:

image

Do you have any idea why it is not "FileList" but "Array"
Can this be cause of the problem?

thanks for advise

@jennifer-shehane
Copy link
Member

Yes, the dataTransfer can definitely be different based on your implementation. Have you tried to walk through some of the debugging steps outlined here? #669 (comment)

@andygock
Copy link

Does anyone have any tips on how one could simulate a drag and drop upload and be able to specify the file contents as well?

Let's say for example, I could predefine define the file contents in a variable.

The above example, at least from what I can see, doesn't say much about defining the actual file data.

@andygock
Copy link

andygock commented Jun 10, 2018

I've actually found a solution to my previous question, referring to this comment:

To drag and drop upload file myfile.csv from fixtures/

Cypress.Commands.add('upload_file', (selector, fileUrl, type = '') => {
  return cy.fixture(fileUrl, 'base64')
    .then(Cypress.Blob.base64StringToBlob)
    .then(blob => {
      const nameSegments = fileUrl.split('/')
      const name = nameSegments[nameSegments.length - 1]
      const testFile = new File([blob], name, { type })
      const event = { dataTransfer: { files: [testFile] } }
      return cy.get(selector).trigger('drop', event)
    })
});

describe("drag and drop upload", () => {
  it("uploads file from filesystem", () => {
    cy.upload_file('.drop', 'myfile.csv');
  });
});

@kutlaykural
Copy link

unfurtunately it does not work for me.
Maybe it is about ember.js. Is there anybody have solution with Ember?

@ZwaarContrast
Copy link

I ended up with something similar to @andygock. I made a gist here: https://gist.github.com/ZwaarContrast/00101934954980bcaa4ae70ac9930c60

This could be refactored to also make use of Cypress.Blob.base64StringToBlob, which I didn't know existed up till now :) Thanks for that!

@DanielStoica85
Copy link

DanielStoica85 commented Jun 27, 2018

Hi @andygock ,
I just tried your solution and, for some reason, it does not work for me. It just stops when it reaches "trigger". It doesn't fail, it doesn't show an error or anything, it just stops.

screen shot 2018-06-27 at 11 26 50

Any idea why?

@akarolewski
Copy link

@DanielStoica85 same here.

@ZwaarContrast
Copy link

@DanielStoica85 and @andygock did you check my gist?

https://gist.github.com/ZwaarContrast/00101934954980bcaa4ae70ac9930c60

Something important to make it work for me was using new window.File([blob], fileName) instead of new File([blob], fileName).

@kutlaykural
Copy link

@ZwaarContrast your solution not works for me. Maybe this caused from Ember.js.
@DanielStoica85 and @andygock same as yours. Which framework do your test run against?

@andygock
Copy link

@kutlaykural @DanielStoica85

Hi,

I haven't had time to look at this, but regarding the framework with my solution I had working, I was working with React and interacting with a react-dropzone component.

Andy

@allanpaiste
Copy link

allanpaiste commented Jul 29, 2018

Personally, I'd sleep much better if the test for this feature would depend directly on any particular implementation. Overall all the solutions still react to "drop" event and the only thing one has to do is to make sure that the provided event is as close to the real thing as possible (by avoiding any mocked objects/functions).

Composed an example for a friend who had an issue with getting drag-and-drop working for his project. Used the same function names and argument formats proposed by @SMenigat.

support/commands.js

const resolveMediaType = (headerContents) => {
    const header = (new Uint8Array(headerContents))
        .subarray(0, 4)
        .reduce((acc, item) => acc + item.toString(16), '');

    switch (header) {
        case '89504e47':
            return 'image/png';
        case '47494638':
            return 'image/gif';
        case 'ffd8ffe0':
        case 'ffd8ffe1':
        case 'ffd8ffe2':
        case 'ffd8ffe3':
        case 'ffd8ffe8':
            return 'image/jpeg';
        case '25504446':
            return 'application/pdf';
        case '504b0304':
            return 'application/zip';
    }

    return 'application/octet-stream';
};

const configureBlob = (blob, name) => {
    return new Cypress.Promise((resolve, reject) => Object.assign(
        new FileReader(), {
            'onloadend': (progress) => resolve(
                Object.assign(
                    blob.slice(0, blob.size, resolveMediaType(progress.target.result)),
                    {name}
                )
            ),
            'onerror': () => reject(null)
        })
        .readAsArrayBuffer(blob.slice(0,4))
    );
};

const createCustomEvent = (eventName, data, files) => {
    const event = new CustomEvent(eventName, {
        bubbles: true,
        cancelable: true
    });

    const dataTransfer = Object.assign(new DataTransfer(), {
        dropEffect: 'move'
    });

    (files || []).forEach(
        file => dataTransfer.items.add(file)
    );

    Object.entries(data || {}).forEach(
        entry => dataTransfer.setData(...entry)
    );
    
    return Object.assign(event, {dataTransfer});
};

/**
 * The added command can either be absolute path to a file or a reference 
 * to Cypress fixture in which case the value should be provided as: 
 * 
 * fixture:some-file.png
 */
const dropFile = (subject, source) => {
    let process = cy.then(() => source);

    if (typeof source === 'string') {
        const segments = source.split(':'),
            type = segments.length > 1 ? segments.shift() : '',
            path = segments.join(':'),
            file = path.split('/').pop();

        cy.log('Drop ' + type + ': ' + path);

        switch (type) {
            case 'fixture':
                process = cy.fixture(file, 'base64')
                    .then(Cypress.Blob.base64StringToBlob)
                    .then(blob => configureBlob(blob, file))
                    .then(blob => new File([blob], file, {type: blob.type}));
        }
    }

    return process
        .then(file => createCustomEvent('drop', {}, [file]))
        .then(event => subject[0].dispatchEvent(event));
}

/**
 * Register the created command to be used on elements
 */
Cypress.Commands.add('dropFile', {prevSubject: 'element'}, dropFile);

Usage examples

/**
 * Usage examples against publicly available uploaders.
 * Requires a fixtures/img.png to be present
 */
context('TinyPNG: drag-and-drop upload', () => {
    beforeEach(() => cy.visit('http://www.tinypng.com'));

    it('converts and provides download link for dropped file', () => {
        cy.get('.upload .target').dropFile('fixture:img.png');

        cy.get('.upload .progress.success').should('be.visible');
        cy.get('.upload .files a').contains('download');
    });
});

context('Ember droplet: drag-and-drop upload', () => {
    beforeEach(() => cy.visit('http://ember-droplet.herokuapp.com'));

    it('displays, validates and uploads the dropped file', () => {
        cy.get('.droppable').dropFile('fixture:img.png');

        cy.get('.droppable .file').should('be.visible');
        cy.get('.counts').contains('Valid: 1');
        cy.get('button').contains('Upload All').click();
        cy.get('.counts').contains('Uploaded: 1');
    });
});

context('AngularJS: drag-and-drop upload #1', () => {
    beforeEach(() => cy.visit('https://angular-file-upload.appspot.com'));

    it('displays, validates and uploads the dropped file', () => {
        cy.get('.drop-box').dropFile('fixture:img.png');

        cy.get('.response').scrollIntoView();

        cy.get('.preview img').should('be.visible');
        cy.get('.response').contains('img.png');
        cy.get('.response').contains('type: image/jpeg');
    });
}); 

context('AngularJS: drag-and-drop upload #2', () => {
    beforeEach(() => cy.visit('http://nervgh.github.io/pages/angular-file-upload/examples/simple'));

    it('displays, validates and uploads the dropped file', () => {
        cy.get('.my-drop-zone').first().dropFile('fixture:img.png');
        cy.get('.my-drop-zone').last().dropFile('fixture:img.png');

        cy.get('.container').contains('Queue length: 2');        
    });
}); 

@kutlaykural
Copy link

@allanpaiste thank you for sharing
but unfortunately, it doesn't work for me (Ember.js)

@allanpaiste
Copy link

Hmmm... I guess the problem that the code might have had was that I did not have MIME type set;

Updated the code above with Ember droplet example.

I guess ideally the MIME type resolver should be part of the dropFixture command.

@kutlaykural
Copy link

yes but i already tried it with type 'image/jpeg' and jpg file

@allanpaiste
Copy link

allanpaiste commented Jul 29, 2018

Do you have an equivalent public site with similar uploader that I could test the whole thing against?

Figured out that the missing piece might have been the fact that created File object name was not properly configured; @kutlaykural - could you try the code again.

@akarolewski
Copy link

akarolewski commented Sep 12, 2018

I tried following @jennifer-shehane guide. Btw, huge thanks for posting this. Will be really helpful in the future :)

This is my code snippet.

Cypress.Commands.add('uploadFile', (fileName, selector) => {
    let dataTransfer = new DataTransfer();
    let files = [];
    const dropEvent = {
        dataTransfer: {
            dropEffect: "none",
            effectAllowed: "all",
            files,
            items: {
                kind: "file",
                type:"image/jpeg"
            },
          types: ["Files"]
        }
    };

    cy.fixture(fileName).then((file) => {
        cy.get(selector).first()
            .trigger('dragover', dropEvent).should('have.class', 'dragover')
            .then(() => {
                const name = fileName.split('/').pop();
                dropEvent.dataTransfer.files.push({
                    name: name,
                    size: 4478976,
                    type: "image/jpeg",
                    webkitRelativePath: ""
                });
            }).trigger('drop', dropEvent);
    });
 };

And that's how I'm using it after.
cy.uploadFile('images/pepsi.jpg', 'div.box-placeholder');

I have this error that I can't really resolve. Any ideas?

Error: coordsHistory must be at least 2 sets of coords at ensureNotAnimating

@abramenal
Copy link

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

@BataevDaniil
Copy link

BataevDaniil commented Apr 24, 2019

Work with react-dropzone v10.1.4

Cypress.Commands.add(
  'dropFiles',
  { prevSubject: true },
  (subject, namesFiles) => {
    const createFile = name => picture =>
      Cypress.Blob.base64StringToBlob(picture, name.replace('.', '/')).then(
        blob => new File([blob], name),
      )
    const createEvent = files => {
      const dropEvent = { dataTransfer: new DataTransfer() }
      files.forEach(file => dropEvent.dataTransfer.items.add(file))
      return dropEvent
    }
    let process = cy
      .fixture(namesFiles[0])
      .then(createFile(namesFiles[0]))
      .then(file => [file])
    for (let i = 1; i < namesFiles.length; i++) {
      process = process.then(files =>
        cy
          .fixture(namesFiles[i])
          .then(createFile(namesFiles[i]))
          .then(file => [...files, file]),
      )
    }
    return process.then(files =>
      cy.wrap(subject).trigger('drop', createEvent(files)),
    )
  },
)

@delvinwidjaja
Copy link

Hi @jennifer-shehane , just wondering if I can use Cypress with ng-file-upload? I've been working on my upload file testing, that uses ng-file-upload and the test always passes an error. Thanks!

@abramenal
Copy link

abramenal commented Jun 26, 2019

@khaldrago96
I've faced some users with ng-file-upload recently, you can take a look on:
abramenal/cypress-file-upload#51

In short, cypress-file-upload works perfectly with ng-file-upload, see its API docs to get started 😈

@delvinwidjaja
Copy link

hi @abramenal , thanks for your response!
I just took a look on your thread and found out its using ng2-file-upload, which is dedicated for Angular2.
Does it work the same way for angularjs and its ng-file-upload ?

@abramenal
Copy link

@khaldrago96
well then you'll be the first one who comes with AngularJS :)
I don't see any reason for the plugin to be not compatible, so you can try this out.
If you face any issue, feel free to submit it and I'll help you chase it!

@bijoutrouvaille
Copy link

bijoutrouvaille commented Apr 10, 2020

I spent several hours today trying to get Filestack to automate. The solution is simple but not obvious: the File class you get inside your Cypress test file seems to be different from the one inside the browser, so the instanceof operator will return false on objects you instantiated with new File. Workaround: grab the browser's window object and use the File class from that, e.g.,

cy.window().then(win => sendFileToFilestackOrWhatever(new win.File([blob], fileName, {type})))

@krinhorn
Copy link

Hello! @jennifer-shehane (or anyone else who knows and sees this) - could you please explain how to see the dataTransfer object, as shown in your screenshot here? I'm not sure which part of the console to click, and then I'm not sure how to drill down into the right object to find what's shown in your screenshot. Thanks!!

@allanpaiste
Copy link

allanpaiste commented Sep 2, 2020

It should be part of the event object that you'd be getting when observing the events that are also listed on the screenshot. So, register an observer for those events (or just one of them) for a the DOM object of your liking, throw in a break-point and the event payload should have that dataTransfer you are looking for :)

@iltera
Copy link

iltera commented May 19, 2021

I've actually found a solution to my previous question, referring to this comment:

To drag and drop upload file myfile.csv from fixtures/

Cypress.Commands.add('upload_file', (selector, fileUrl, type = '') => {
  return cy.fixture(fileUrl, 'base64')
    .then(Cypress.Blob.base64StringToBlob)
    .then(blob => {
      const nameSegments = fileUrl.split('/')
      const name = nameSegments[nameSegments.length - 1]
      const testFile = new File([blob], name, { type })
      const event = { dataTransfer: { files: [testFile] } }
      return cy.get(selector).trigger('drop', event)
    })
});

describe("drag and drop upload", () => {
  it("uploads file from filesystem", () => {
    cy.upload_file('.drop', 'myfile.csv');
  });
});

@andygock
Your solution works for me and should work with any other drag-n-drop upload plugins out there! Just brilliant!!!
Thank you very much for sharing :)

By the way, I made your code working with vue-dropzone and nuxt-dropzone plugins.

@natre01
Copy link

natre01 commented Aug 23, 2022

I'm trying to test the drag and drop of a folder recursively. I could create an implementation of a DataTransfer object with every folder/subfolder/file being defined, but it would be very ugly very fast. Does anyone know of an easy way to do it? My folder would simply be fixtures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg/driver This is due to an issue in the packages/driver directory type: question
Projects
None yet
Development

No branches or pull requests