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:
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
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.