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 Senior Developer working with Ruby/Rails and JavaScript. He’s an active open source contributor and the lead developer for Interactor. Steve is also involved in documenting and improving Collective Idea’s software development practices.

Comments

  1. radiga@onedlp.com
    rad
    February 11, 2012 at 12:06 PM

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

  2. ngoccong244@gmail.com
    CongDang
    May 31, 2012 at 10:00 AM

    Any sample code for java guys?

  3. artemave@gmail.com
    artem
    March 21, 2013 at 18:07 PM

    Very helpful, thanks!

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

  4. March 21, 2013 at 18:27 PM

    @artem: Thanks, and fixed!

  5. artemave@gmail.com
    artem
    March 26, 2013 at 17:35 PM

    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. clabrunda@gmail.com
    Chris LaBrunda
    November 25, 2013 at 16:26 PM

    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.

  7. mangala176@gmail.com
    mangala
    February 11, 2015 at 6:26 AM

    Any Configuration for Internet Explorer for storing downloaded files to specific path?

  8. gpumple@adjuggler.com
    Gabe Pumple
    April 17, 2015 at 20:28 PM

    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.

  9. steve@collectiveidea.com
    Steve
    April 17, 2015 at 21:01 PM

    Gabe: Sorry, I’m stumped on that one!

  10. cjohnson@instructure.com
    Chris
    April 27, 2015 at 16:10 PM

    Any suggestion on how to make this work for Safari?

  11. ramugogurla@gmail.com
    ramu
    August 04, 2015 at 14:16 PM

    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

  12. sara@rediff.com
    sarala
    March 26, 2018 at 21:32 PM

    I am trying to download an excel and verify the contents. Chrome profile done. Added downloads helper file. When I run test, I am getting uninitialized constant DownloadHelpers::Rails (NameError). Please advice

  13. maltem
    May 07, 2018 at 12:23 PM

    Instead of creating a new profile when setting up the driver you can also use DesiredCapabilities and ChromeOptions to pass in the “download.default_directory” profile setting.

    See https://sites.google.com/a/chromium.org/chromedriver/capabilities

    We needed to do this as we were already passing in the window-size command-line argument and creating a new profile caused the window-size to be ignored or overridden.

  14. Peter
    December 11, 2018 at 2:22 AM

    Thanks for this! This doesn’t seem to work on headless chrome. Any advice?

  15. Larry
    May 09, 2019 at 20:29 PM

    @Peter

    if headless
    bridge = driver.browser.send(:bridge)
    path = “/session/#{bridge.session_id}/chromium/send_command”
    bridge.http.call(:post, path, cmd: ‘Page.setDownloadBehavior’,
    params: {
    behavior: ‘allow’,
    downloadPath: DownloadHelpers::PATH
    })
    end

    details here: https://bugs.chromium.org/p/chromium/issues/detail?id=696481