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.
Comments
first timer .. loved ur site .. loved ur approaches ..
Any sample code for java guys?
Very helpful, thanks!
typo: there is an extra ‘s’ in ‘World(DownloadsHelpers)’
@artem: Thanks, and fixed!
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
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.
Any Configuration for Internet Explorer for storing downloaded files to specific path?
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: Sorry, I’m stumped on that one!
Any suggestion on how to make this work for Safari?
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
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
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.
Thanks for this! This doesn’t seem to work on headless chrome. Any advice?
@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