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.

deepwell
  • 20,195
  • 10
  • 33
  • 39

5 Answers5

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.

deepwell
  • 20,195
  • 10
  • 33
  • 39
  • `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 } });` – cmititiuc Oct 31 '18 at 03:17
  • 1
    This is a complete and total blessing. Thank you. – a2f0 Nov 21 '18 at 17:35
  • 2
    Update from May 2019: This still works perfectly with no changes required. Thanks so much! – ndbroadbent May 17 '19 at 12:59
  • 1
    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();` – ChrisCPO Jul 02 '19 at 18:01
  • 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 https://github.com/pikselpalette/dropybara – Alan Sep 17 '19 at 07:36
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.

woto
  • 2,893
  • 1
  • 29
  • 24
  • 1
    Instead of `Rails.root.join('spec/fixtures/files/avatar.png')` you should be able to do `file_fixture('avatar.png')`. – Brendon Muir May 03 '21 at 20:49
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.

pixelearth
  • 13,674
  • 10
  • 62
  • 110
4

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.

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);
  });
};
ppires
  • 233
  • 1
  • 8
  • OP is asking for capybara, not JS. This would be better done as a gist on a comment for the selected answer. – grepsedawk Dec 17 '17 at 17:41
  • 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" attribute – The Lazy Log Jan 08 '18 at 04:25