How do you test uploading a file with Capybara and Dropzone.js?
Asked Answered
F

5

19

I've switched to using the Dropzone.js plugin for drag-and-drop file uploads. How can I write a Capybara test to ensure this functionality keeps on working?

Previously I had a template with an input file element:

<input type="file" name="attachments">

And the test was simple:

When(/^I upload "([^"]*)"$/) do |filename|
  attach_file("attachments", File.expand_path(filename))
  # add assertion here
end

However this no longer works because Dropzone doesn't have a visible file input.

Fariss answered 1/10, 2015 at 6:10 Comment(0)
F
26

To solve this, simulate a drop event to trigger dropping an attachment onto Dropzone. First add this function to your step definition:

    # Upload a file to Dropzone.js
    def drop_in_dropzone(file_path)
      # Generate a fake input selector
      page.execute_script <<-JS
        fakeFileInput = window.$('<input/>').attr(
          {id: 'fakeFileInput', type:'file'}
        ).appendTo('body');
      JS
      # Attach the file to the fake input selector
      attach_file("fakeFileInput", file_path)
      # Add the file to a fileList array
      page.execute_script("var fileList = [fakeFileInput.get(0).files[0]]")
      # Trigger the fake drop event
      page.execute_script <<-JS
        var e = jQuery.Event('drop', { dataTransfer : { files : [fakeFileInput.get(0).files[0]] } });
        $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
      JS
    end

Then test with:

    When(/^I upload "([^"]*)"$/) do |filename|
      drop_in_dropzone File.expand_path(filename)
      # add assertion here
    end

NOTE: You need to have jQuery loaded, and the Dropzone element requires the dropzone class.

Fariss answered 1/10, 2015 at 6:10 Comment(5)
var e = jQuery.Event('drop', { dataTransfer : { files : [fakeFileInput.get(0).files[0]] } }); Can't you just do var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });Ineptitude
This is a complete and total blessing. Thank you.Sil
Update from May 2019: This still works perfectly with no changes required. Thanks so much!Assure
I changed last chunk for programmatically accessing dropzone and removed the fake input so you can call it more then once on the same zone. var e = jQuery.Event("drop", { dataTransfer : { files : [fakeFileInput.get(0).files[0]] } }); Dropzone.instances[0].listeners[0].events.drop(e); $("#fakeFileInput").remove();Greathouse
I couldn't get this working, it was saying fakeFileInput.get isn't a function. I presume I'm not loading jquery correctly in my tests? I found this gem which is working well github.com/pikselpalette/dropybaraJerkin
S
8

These days I find this way more graceful

page.attach_file(Rails.root.join('spec/fixtures/files/avatar.png')) do
  page.find('#avatar-clickable').click
end

Where is in my case #avatar-clickable is a div which contain Dropzone form tag.

Stabilize answered 24/6, 2020 at 9:30 Comment(1)
Instead of Rails.root.join('spec/fixtures/files/avatar.png') you should be able to do file_fixture('avatar.png').Sulk
H
6

Since Capybara 3.21.0, you can drop files on elements like this:

find(".dropzone").drop(Rails.root.join("spec/fixtures/file.txt"))

See the Element#drop source for details.

Halve answered 12/7, 2021 at 8:15 Comment(0)
E
4

Building off of @deepwell's answer which didn't quite work for me, here is a solution using vanilla JS for the events and event dispatching, and a neutral selector for the dropzone:

  def drop_in_dropzone(file_path, zone_selector)
    # Generate a fake input selector
    page.execute_script <<-JS
      fakeFileInput = window.$('<input/>').attr(
        {id: 'fakeFileInput', type:'file'}
      ).appendTo('body');
    JS

    # Attach the file to the fake input selector
    attach_file("fakeFileInput", file_path)

    # Add the file to a fileList array
    page.execute_script("fileList = [fakeFileInput.get(0).files[0]]")

    # Trigger the fake drop event
    page.execute_script <<-JS
      dataTransfer = new DataTransfer()
      dataTransfer.items.add(fakeFileInput.get(0).files[0])
      testEvent = new DragEvent('drop', {bubbles:true, dataTransfer: dataTransfer })
      $('#{zone_selector}')[0].dispatchEvent(testEvent)
    JS
  end

uses global vars on purpose, so I could test in js console, but feel free to scope them.

Elledge answered 4/6, 2019 at 19:14 Comment(0)
S
3

In case anyone is interested, I ported @deepwell's function to javascript, to use it with javascript flavoured selenium:

this.dropInDropzone = function(filePath) {
  var script = "fakeFileInput = $('#fakeFileInput'); if (fakeFileInput.length === 0) fakeFileInput = window.$('<input/>').attr({id: 'fakeFileInput', type:'file'}).appendTo('body');";
  // Generate a fake input selector
  return driver.executeScript(script).then(function() {
    // Attach the file to the fake input selector
    return driver.findElement(webdriver.By.css('#fakeFileInput')).sendKeys(filePath);
  }).then(function() {
    // Add the file to a fileList array
    return driver.executeScript("var fileList = [fakeFileInput.get(0).files[0]]");
  }).then(function() {
    // Trigger the fake drop event
    script = "var e = jQuery.Event('drop', { dataTransfer : { files : [fakeFileInput.get(0).files[0]] } }); $('.dropzone')[0].dropzone.listeners[0].events.drop(e);"
    return driver.executeScript(script);
  });
};
Sepia answered 6/7, 2016 at 22:40 Comment(2)
OP is asking for capybara, not JS. This would be better done as a gist on a comment for the selected answer.Puckery
It seems each page might implement the dropzone differently and we need to custom this script a bit to make it work. In my case, it is something related to the "originalEvent" attributeHelminthic

© 2022 - 2024 — McMap. All rights reserved.