Testing File Downloads with Capybara and ChromeDriver

Photo by Tambako the Jaguar, used under Creative Commons https://flic.kr/p/e9AnTA

At Collective Idea, we Cucumber, Capybara and ChromeDriver… and alliteration. But we recently encountered an issue with a very Ajaxy Rails app where we need to test a file download and assert its content.

Our scenario looks like:

Scenario: Exporting the fruits list
  Given the following fruits exist:
    | Name   | Color  |
    | Apple  | Red    |
    | Orange | Orange |
    | Lemon  | Yellow |
  And I am on the fruits page
  When I follow "Export"
  Then the downloaded file content should be:
    """
    Name,Color
    Apple,Red
    Orange,Orange
    Lemon,Yellow
    """

Easy enough! Early in the app’s life, we wrote this Cucumber step:

Then "the downloaded file content should be:" do |content|
  page.response_headers["Content-Disposition"].should == "attachment"
  page.source.should == content
end

This worked like gangbusters. But with such an Ajaxy app, we soon moved to ChromeDriver as our default Capybara driver and our nice green scenario turned an annoying shade of red.

When the scenario ran, Chrome triggered the download as expected but threw the file into my “Downloads” directory. Capybara had no reference to its content and to make matters worse, Cucumber didn’t wait for the download to finish before moving on.

After much frustration…

We discovered that it’s possible to provide a Chrome profile (just a collection of settings) when registering the :chrome Capybara driver. We’re registering the driver in features/support/chromedriver.rb so we added the profile there:

require "selenium/webdriver"

Capybara.register_driver :chrome do |app|
  profile = Selenium::WebDriver::Chrome::Profile.new
  profile["download.default_directory"] = DownloadHelpers::PATH.to_s
  Capybara::Selenium::Driver.new(app, :browser => :chrome, :profile => profile)
end

Capybara.default_driver = Capybara.javascript_driver = :chrome

We added a download.default_directory setting to the profile. This tells the browser where to send downloaded files. Eureka!

That answers the question of downloading the file to the proper place, but we still need to make sure we wait for the download to finish. We take care of that in features/support/downloads.rb:

module DownloadHelpers
  TIMEOUT = 10
  PATH    = Rails.root.join("tmp/downloads")

  extend self

  def downloads
    Dir[PATH.join("*")]
  end

  def download
    downloads.first
  end

  def download_content
    wait_for_download
    File.read(download)
  end

  def wait_for_download
    Timeout.timeout(TIMEOUT) do
      sleep 0.1 until downloaded?
    end
  end

  def downloaded?
    !downloading? && downloads.any?
  end

  def downloading?
    downloads.grep(/\.crdownload$/).any?
  end

  def clear_downloads
    FileUtils.rm_f(downloads)
  end
end

World(DownloadHelpers)

Before do
  clear_downloads
end

After do
  clear_downloads
end

Now we’re equipped with everything we need to effectively manage and inspect file downloads. Our Cucumber step simply changes to:

Then "the downloaded file content should be:" do |content|
  download_content.should == content
end

And there you have it. It’s a little bit of added support code but if you’re dealing with downloads, it’s well worth your while.

Photo of Steve Richert

Steve is a Michigan State grad in mechanical engineering, but has been programming since he was in single digits with his Commodore 64. QBasic greatness followed.

After putting in his time with PHP, Steve discovered Ruby, Rails, Git and Agile development. Open source greatness followed. After long admiring their work, Steve can finally cross working for Collective Idea off his bucket list.

Steve lives in Grandville with his wonderful wife, Catie. Just look for the really tall guy running with the really short woman.

Comments:


Post a Comment

(optional)
(optional — will be included as a link.)
  1. first timer .. loved ur site .. loved ur approaches ..

    rad
    rad
    February 11, 2012 at 7:06 AM
  2. Any sample code for java guys?

    CongDang
    CongDang
    May 31, 2012 at 6:00 AM
  3. Very helpful, thanks!

    typo: there is an extra ‘s’ in ‘World(DownloadsHelpers)’

    artem
    artem
    March 21, 2013 at 14:07 PM
  4. @artem: Thanks, and fixed!

    March 21, 2013 at 14:27 PM
  5. Here is firefox profile:

    Capybara.register_driver :firefox do |app|
      profile = Selenium::WebDriver::Firefox::Profile.new
      profile[‘browser.download.dir’] = DownloadHelpers::PATH.to_s
      # means save to the ‘browser.download.dir’ as opposed to ~/Downloads
      profile[‘browser.download.folderList’] = 2
      # prevents “open with” dialog
      profile[‘browser.helperApps.neverAsk.saveToDisk’] = ‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet’

      Capybara::Selenium::Driver.new(app, :browser => :firefox, :profile => profile)
    end

    artem
    artem
    March 26, 2013 at 13:35 PM
  6. We’ve been using this successfully for a while, but noticed some intermittent failures on some of our download tests.  The problem manifested as the downloaded file being empty when our test examined it.

    To fix it, I increased the timeout, but more importantly I switched the order of the test to see if the file finished downloading.  Your `downloaded?` function checks to see if chrome’s partial download file marker does not exist, then checks to see if any files exist after that.  It’s possible that the file could start downloading between the two tests, however, which could lead to the function falsely returning true.  My `downloaded?` function instead looks like:

      def downloaded?
        downloads.any? && !downloading?
      end

    This has been passing consistently for the past two weeks.  Hope this helps someone.

    Chris LaBrunda
    Chris LaBrunda
    November 25, 2013 at 11:26 AM
  7. Any Configuration for Internet Explorer for storing downloaded files to specific path?

    mangala
    mangala
    February 11, 2015 at 1:26 AM
  8. Thanks so much, found this super helpful. One question though: do you have any idea how to change the chrome profile so that the files won’t be shown down at the bottom of the browser window? I am having a problem where chrome can’t click an element because once a file is downloaded the window is slightly smaller.

    Gabe Pumple
    Gabe Pumple
    April 17, 2015 at 16:28 PM
  9. Gabe: Sorry, I’m stumped on that one!

    Steve
    Steve
    April 17, 2015 at 17:01 PM
  10. Any suggestion on how to make this work for Safari?

    Chris
    Chris
    April 27, 2015 at 12:10 PM
  11. Capybara.register_driver :Chrome do |app|
    profile = Selenium::WebDriver::Chrome::Profile.new
    profile[“download.default_directory”] = DownloadHelpers::PATH.download_directory
    Capybara::Selenium::Driver.new(app, :browser => :Chrome, :profile => profile)
    end
    Capybara.default_driver = Capybara.javascript_driver = :Chrome

    ramu
    ramu
    August 04, 2015 at 10:16 AM