Posted 2 months ago. Visible to the public. Linked content. Auto-destruct in 10 days

Updated: Cucumber: Testing file downloads with Selenium

Added two more methods:

Method 3: Use a request spec

Only test that the download link's [href] attribute points to a particular route.

Then test that route with a request spec. Or, if the uploaded file lives in public/, test that the linked file exists.

Method 4: Test without Selenium

If you can get away with it, run that one test with Rack::Test instead of Selenium. With a Rack::Test driver Capybara allows some access to the response headers via page.driver.response.headers.

Does your version of Ruby on Rails still receive security updates?
Rails LTS provides security patches for old versions of Ruby on Rails (3.2 and 2.3).

Changes

  • -Below you can find two methods how to test file downloads with Selenium and Capybara. Both have different advantages and drawbacks.
  • +Below you can find multiple methods how to test file downloads with Selenium and Capybara. Both have different advantages and drawbacks.
  • ## Method 1: Looking into the download folder of the test browser
  • Pros:
  • - Resembles most closely what happens in the real world.
  • - You can inspect the file with Ruby code in any way you want.
  • Cons:
  • - Does not work well with CI setups (because the browser might not be on the same container as cucumber)
  • - Solution is specific to Chrome.
  • Note: The step below will override a [spreewald step](https://github.com/makandra/spreewald/blob/474caed6c28ec7eac50ea48894527760cefb3118/lib/spreewald/web_steps.rb#L369) and allows to use Regex for the filename. You might want to rename this step within you project in case you are using [Spreewald](https://github.com/makandra/spreewald).
  • *features/step_definitions/spreewald_overrides_steps.rb*
  • ```
  • Then(/^I should get a download with filename "(.*?)"$/) do |filename|
  • if Capybara.current_driver == :selenium
  • patiently do
  • expect(DownloadHelpers.last_download&.basename&.to_s).to match(filename)
  • end
  • else
  • expect(response_headers['Content-Disposition']).to match(/filename=\"#{filename}\"/)
  • end
  • end
  • ```
  • *features/support/download_helpers.rb*
  • ```
  • # This is an adapted approach from
  • # https://collectiveidea.com/blog/archives/2012/01/27/testing-file-downloads-with-capybara-and-chromedriver
  • module DownloadHelpers
  • TIMEOUT = 10
  • module_function
  • def download_path
  • download_path = Rails.root.join("tmp/test_downloads#{ENV['TEST_ENV_NUMBER']}")
  • FileUtils.mkdir_p(download_path)
  • download_path
  • end
  • def clear_downloads
  • FileUtils.rm_r(download_path) if File.exist?(download_path)
  • end
  • def last_download
  • downloads = Pathname.new(download_path)
  • Timeout.timeout(TIMEOUT) do
  • sleep 0.1 until downloads.glob('*.crdownload').blank?
  • end
  • downloads.children.sort_by(&File.method(:ctime)).last
  • end
  • end
  • Before do
  • DownloadHelpers.clear_downloads
  • end
  • After do
  • DownloadHelpers.clear_downloads
  • end
  • ```
  • *features/support/selenium.rb*
  • ```
  • Capybara.register_driver :selenium do |app|
  • options = Selenium::WebDriver::Chrome::Options.new
  • options.add_argument('--mute-audio')
  • options.add_argument('--disable-infobars')
  • options.add_preference('credentials_enable_service', false)
  • options.add_preference('profile.password_manager_enabled', false)
  • options.add_preference('download.default_directory', DownloadHelpers.download_path)
  • Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
  • end
  • Before do
  • Capybara.current_driver = :rack_test
  • end
  • Before('@javascript') do
  • Capybara.current_driver = :selenium
  • end
  • ```
  • ## Method 2: Inspecting the file using JavaScript
  • Pros:
  • - Works with CI.
  • - Browser-agnostic.
  • Cons:
  • - You don't have access to the physical file in Ruby.
  • - Works only with links, not with downloads triggered by JavaScript.
  • The idea is to request the file via the browser's fetch API, check that is downloads correctly, and return the file name (and possibly the file content) to cucumber.
  • Use the following step definition:
  • ```ruby
  • When('I download {string}') do |link|
  • if Capybara.current_driver == :selenium
  • href = find_link(link)['href']
  • result = page.evaluate_async_script(<<~JS, href)
  • let [url, done] = arguments
  • fetch(url).then((response) => {
  • if (response.status >= 400) {
  • done({ error: 'download failed' })
  • } else {
  • window.lastResponse = response
  • done({ content_disposition: response.headers.get('Content-Disposition') })
  • }
  • })
  • JS
  • expect(result['error']).to eq(nil), result['error']
  • @content_disposition = result['content_disposition']
  • else
  • @content_disposition = response_headers['Content-Disposition']
  • end
  • end
  • Then('the download should have the filename {string}') do |filename|
  • expect(@content_disposition).to match(/filename=\"#{filename}\"/)
  • end
  • Then('the downloaded file content should include the following lines:') do |table|
  • if Capybara.current_driver == :selenium
  • file_content = page.evaluate_async_script(<<~JS)
  • let [done] = arguments
  • lastResponse.text().then((text) => {
  • done(text)
  • })
  • JS
  • single_line_file = file_content.gsub("\n", '').gsub(' ', '')
  • table.raw.each do |row|
  • expect(single_line_file).to include(*row), "expected \n file to include \n #{row} \n"
  • end
  • else
  • raise 'not implemented for driver'
  • end
  • end
  • ```
  • Use it like this:
  • ```
  • When I download "download me"
  • Then the download should have the filename "export.csv"
  • And the downloaded file content should include the following lines:
  • """
  • Name, E-mail
  • Mr. Foo, mr.foo@example.com
  • """
  • ```
  • +
  • +## Method 3: Use a request spec
  • +
  • +Only test that the download link's `[href]` attribute points to a particular route.
  • +
  • +Then test that route with a request spec. Or, if the uploaded file lives in `public/`, test that the linked file exists.
  • +
  • +## Method 4: Test without Selenium
  • +
  • +If you can get away with it, run that one test with `Rack::Test` instead of Selenium. With a `Rack::Test` driver Capybara allows some access to the response headers via `page.driver.response.headers`.

Owner of this card:

Avatar
Henning Koch
Last edit:
2 months ago
by Henning Koch
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more