Testing File Downloads with Capybara and ChromeDriver
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.

5 Comments
rad February 11, 2012 http://www.onedlp.cpm
first timer .. loved ur site .. loved ur approaches ..
CongDang May 31, 2012
Any sample code for java guys?
artem March 21, 2013
Very helpful, thanks!
typo: there is an extra ‘s’ in ‘World(DownloadsHelpers)’
Steve Richert March 21, 2013 http://collectiveidea.com
@artem: Thanks, and fixed!
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
Post a Comment