Collective Idea

Collective Idea Logo

Steve Richert

Testing File Downloads with Capybara and ChromeDriver

By Steve Richert on January 27, 2012 in capybara, chromedriver, and cucumber

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.

By Steve Richert on January 27, 2012 in capybara, chromedriver, and cucumber

6 Comments

  1. rad

    rad February 11, 2012 http://www.onedlp.cpm

    first timer .. loved ur site .. loved ur approaches ..

  2. CongDang

    CongDang May 31, 2012

    Any sample code for java guys?

  3. artem

    artem March 21, 2013

    Very helpful, thanks!

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

  4. Steve Richert

    Steve Richert March 21, 2013 http://collectiveidea.com

    @artem: Thanks, and fixed!

  5. artem

    artem March 26, 2013

    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

  6. Chris LaBrunda

    Chris LaBrunda November 25, 2013

    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.

Post a Comment

Contact Us

Find us on Google Maps
Collective Idea
44 East 8th Street, Suite 410
Holland, Michigan 49423 USA 42.790334-86.105251

Follow us on the Interwebs

We are currently available for medium and long term projects. Please get in touch if we can be of service.