cuprite icon indicating copy to clipboard operation
cuprite copied to clipboard

How to test file download?

Open zedtux opened this issue 3 years ago • 11 comments

I have a test where something is exported as a PDF file and pushed to the client so that he downloads that generated PDF file.

How can I test that and test that the expected downloaded file name?

zedtux avatar Nov 24 '21 21:11 zedtux

Hey @zedtux I think you are looking for https://github.com/teamcapybara/capybara/blob/f7ab0b5cd5da86185816c2d5c30d58145fe654ed/lib/capybara/spec/session/click_link_spec.rb#L219

route avatar Nov 25 '21 05:11 route

Thank you @route for that link, I will use it in my tests 👍

zedtux avatar Nov 25 '21 13:11 zedtux

So I tried to use that code but it doesn't work as it can't find the download file.

My generated file is done from a JavaScript app. How is Chrome/Cuprite saving the downloads to the Capybara.save_path ? Is there anything to setup in order to have it working?

zedtux avatar Nov 25 '21 21:11 zedtux

You just have to setup save_path dir and that's basically it. Run the test with CUPRITE_DEBUG=true and show the output. Also tell details regarding specs, do you run everything on the same machine or it's docker setup...

route avatar Nov 28 '21 07:11 route

Some hints from other repo https://github.com/rubycdp/ferrum/issues/169

route avatar Nov 28 '21 07:11 route

Thank you @route for the help, unfortunately it doesn't work, I never have downloaded files appearing.

I'm using a Docker environment with a compose service for my app, and another one for Chrome using the browserless/chrome:1.49-chrome-stable Docker image. I've basically followed the excellent blog article System of a test: Proper browser testing in Ruby on Rails from Evil Martians.

I'm running a Cucumber feature which downloads a PDF file so I have a features/support/capybara_setup.rb file like this:

# frozen_string_literal: true

# Capybara settings (not covered by Rails system tests)

# Make server listening on all hosts
Capybara.server_host = 'rails'

Capybara.server_port = 3001

Capybara.always_include_port = true

Capybara.server = :puma

Capybara.disable_animation = true

# Replaces the environment action mailer configuration in order to match the
# Capybara's one, making email links pointing to the Capybara server.
Rails.configuration.action_mailer.default_url_options[:host] = [
  Capybara.server_host,
  Capybara.server_port
].join(':')

# Don't wait too long in `have_xyz` matchers
Capybara.default_max_wait_time = ENV['CI'] == true ? 15 : 2

# Normalizes whitespaces when using `has_text?` and similar matchers
Capybara.default_normalize_ws = true

# Where to store artifacts (e.g. screenshots, downloaded files, etc.)
Capybara.save_path = Rails.root.join(ENV.fetch('CAPYBARA_ARTIFACTS', 'tmp/capybara'))

a features/support/cuprite_setup.rb file like this:

# frozen_string_literal: true

# Cuprite is a modern Capybara driver which uses Chrome CDP API
# instead of Selenium & co.
# See https://github.com/rubycdp/cuprite

#
# Custom Ferrum gem logger in order to make features/support/browser_log.rb
# still working.
#
class FerrumLogger
  attr_reader :logs

  def initialize
    @logs = []
  end

  def clear
    @logs = []
  end

  def puts(log_str)
    log_body = parse(log_str)

    return if not_looking_at?(log_body)

    @logs << build_selenium_like_log_from(log_body)
  end

  private

  def build_log_message_from(body)
    "#{body['params']['entry']['url']} - #{body['params']['entry']['text']}"
  end

  def build_selenium_like_log_from(body)
    OpenStruct.new(message: build_log_message_from(body),
                   level: body['params']['entry']['level'])
  end

  def parse(string)
    _log_symbol, _log_time, log_body_str = string.strip.split(' ', 3)
    JSON.parse(log_body_str)
  end

  def not_looking_at?(body)
    unless %w[
      Runtime.exceptionThrown
      Log.entryAdded
      Runtime.consoleAPICalled
    ].include?(body['method'])
      return true
    end

    return true unless body['params'].key?('entry')

    body['params']['entry'] == 'error'
  end
end

REMOTE_CHROME_URL = ENV['CHROME_URL']
REMOTE_CHROME_HOST, REMOTE_CHROME_PORT =
  if REMOTE_CHROME_URL
    URI.parse(REMOTE_CHROME_URL).yield_self do |uri|
      [uri.host, uri.port]
    end
  end

# Check whether the remote chrome is running and configure the Capybara
# driver for it.
remote_chrome =
  begin
    if REMOTE_CHROME_URL.nil?
      false
    else
      Socket.tcp(REMOTE_CHROME_HOST, REMOTE_CHROME_PORT, connect_timeout: 1)
            .close
      true
    end
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
    false
  end

remote_options = remote_chrome ? { url: REMOTE_CHROME_URL } : {}

require 'capybara/cuprite'

Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(
    app,
    **{
      browser_options: remote_chrome ? { 'no-sandbox' => nil } : {},
      inspector: true,
      logger: FerrumLogger.new,
      js_errors: true,
      save_path: Capybara.save_path,
      window_size: [810, 1080]
    }.merge(remote_options)
  )
end

Capybara.default_driver = Capybara.javascript_driver = :cuprite

# Add shortcuts for cuprite-specific debugging helpers
module CupriteHelpers
  def pause
    page.driver.pause
  end

  def debug(binding = nil)
    $stdout.puts '🔎 Open Chrome inspector at http://localhost:3333'

    return binding.pry if binding

    page.driver.pause
  end
end

World(CupriteHelpers)

a features/support/download_helpers.rb file:

# frozen_string_literal: true

module System
  module DownloadHelpers
    TIMEOUT = 10

    def downloads
      Dir[Capybara.save_path.join('*')].reject { |f| File.directory?(f) }
    end

    def download
      downloads.first
    end

    def download_content
      wait_for_download
      File.read(download)
    end

    def wait_for_download
      Timeout.timeout(TIMEOUT, Timeout::Error) 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
end

World(System::DownloadHelpers)

but when I should have a file downloaded (the test clicks a button which send the file to the web browser, which was working with Selenium) nothing appear to be downloaded. Also using the wait_for_download method, right after having clicked the button, fails with a execution expired (Timeout::Error) error.

zedtux avatar Dec 02 '21 19:12 zedtux

I'll take a look tomorrow

route avatar Dec 02 '21 19:12 route

Excellent, thank you! If you need anything from me, just let me know.

zedtux avatar Dec 02 '21 19:12 zedtux

My first guess is that Cuprite is well saving the file at the given position but in the chrome service, but the tests are running in the app service.

I will try to redo the same I did with Selenium which is to create a volume in compose in order to share the chrome save_path with the app service and see if it works.

zedtux avatar Dec 03 '21 06:12 zedtux

You have a great setup! All well thought and tidy. As for download behavior if Chrome runs in a dedicated container then the file is downloaded there definitely because under the hood it uses Download which simply puts the file into downloadPath. So I'm afraid it lies in the docker container for Chrome. I think you can solve it with mounting the same volume for both containers say like to /tmp. Share your docker compose files with us, it can be helpful in the future so I can link to this issue.

route avatar Dec 03 '21 10:12 route

Sure I will.

zedtux avatar Dec 03 '21 10:12 zedtux