Downloading file to specific folder using Capybara and Poltergeist driver - capybara

I am writing my acceptance tests using Capybara and Poltergeist driver.I need to validate the content of the CSV file downloaded.
I tried various ways of rendering the content on the page itself instead of downloading it.
Also tried changing the mime types, but it is not working.
Finally I want to settle down with the option of downloading the file in a specific folder and then read the CSV file using core ruby libraries.
In order to achieve this,when poltergeist driver clicks on download link then I want it to handle the pop-up and download the file directly in the given folder.
In Selenium's chrome and firefox drivers, I have option of configuring profiles to handle pop ups and configure download directory.
Is there any such option using poltergeist? Any information will be helpful.

It is not possible with Poltergeist, you can just check the headers.
step 'I should get zipped file' do
page.response_headers['Content-Disposition'].should include("filename=\"file.zip\"")
end
But is is possible with Chrome driver and also with recent versions of Firefox and Selenium Webdriver. Unfortunately it runs via Selenium - i.e. not headless... See this article: http://collectiveidea.com/blog/archives/2012/01/27/testing-file-downloads-with-capybara-and-chromedriver/
My approach - slightly different as I'm working with Spinach and Rubyzip:
Add the following to your Gemfile
group :test do
gem 'chromedriver-helper' # for Chrome <= 28
gem 'chromedriver2-helper' # for Chrome >= 29
gem 'selenium-webdriver'
end
features/support/capybara.rb - I'm using Poltergeist for scenarios with #javascript tag and Chrome for scenarios with #download tag.
require 'spinach/capybara'
require 'capybara/poltergeist'
require 'selenium/webdriver'
# ChromeDriver 1.x, for Chrome <= 28
Capybara.register_driver :chrome do |app|
profile = Selenium::WebDriver::Chrome::Profile.new
profile['download.default_directory'] = DownloadHelper::PATH.to_s
args = ["--window-size=1024,768"]
Capybara::Selenium::Driver.new(app, browser: :chrome, profile: profile, args: args)
end
# ChromeDriver 2.x, for Chrome >= 29
Capybara.register_driver :chrome do |app|
prefs = {
download: {
prompt_for_download: false,
default_directory: DownloadHelper::PATH.to_s
}
}
args = ['--window-size=1024,768']
Capybara::Selenium::Driver.new(app, browser: :chrome, prefs: prefs, args: args)
end
# Tested with Firefox 27 and Selenium Webdriver 2.39
Capybara.register_driver :firefox do |app|
profile = Selenium::WebDriver::Firefox::Profile.new
profile['browser.download.dir'] = DownloadHelper::PATH.to_s
profile['browser.download.folderList'] = 2 # 2 - save to user defined location
profile['browser.helperApps.neverAsk.saveToDisk'] = 'application/zip'
Capybara::Selenium::Driver.new(app, browser: :firefox, profile: profile)
end
Capybara.javascript_driver = :poltergeist # :webkit :selenium :poltergeist :chrome
Spinach.hooks.on_tag("javascript") do
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 5
end
Spinach.hooks.on_tag("download") do
Capybara.current_driver = :chrome # or :firefox
Capybara.default_wait_time = 50
end
features/support/downloads.rb
module DownloadHelper
TIMEOUT = 10
PATH = Rails.root.join("tmp/downloads")
extend self
def downloads
Dir[PATH.join("*")]
end
def download_path
wait_for_download
downloads.first
end
def download_content
wait_for_download
File.read(download_path)
end
def wait_for_download
Timeout.timeout(TIMEOUT) do
sleep 0.1 until downloaded?
end
end
def downloaded?
downloads.any? && !downloading?
end
def downloading?
downloads.grep(/\.crdownload$/).any?
end
def clear_downloads
FileUtils.rm_f(downloads)
end
end
Spinach.hooks.before_scenario do |scenario|
DownloadHelper.clear_downloads
end
Spinach.hooks.after_scenario do
DownloadHelper.clear_downloads
end
features/file_download.feature
Feature: File download
As a user
I want to be able to download my files
Background:
Given I am logged in as a user
And I have uploaded files in the system
#download
Scenario: Successfull download
When I click on the download button
Then I should get zipped files
features/steps/file_download.rb - Note that you can't use page.response_headers as it is not supported by the Selenium/ChromeDriver. But you can check the filename of the downloaded file using the File.basename().
class Spinach::Features::FileDownload < Spinach::FeatureSteps
include SharedAuthentication
step 'I click on the download button' do
click_link "Download"
end
step 'I should get zipped files' do
File.basename(DownloadHelper.download_path).should == 'file.zip'
Zip::ZipFile.open(DownloadHelper.download_path) do |zipfile|
zipfile.find_entry('myfile.txt').should_not be_nil
zipfile.find_entry('myphoto.jpg').should_not be_nil
end
end
end

I've had to do similar things in my rails app. My solution is using Javascript to make a XMLHttpRequest to the URL, downloading the file, returning the contents of the file back to Capybara, and using ruby to save the file somewhere on disk. Then in another step, I check the contents to the downloaded CSV file.
Here's the step definition for downloading the file:
Then /^I download the csv file$/ do
page.execute_script("window.downloadCSVXHR = function(){ var url = window.location.protocol + '//' + window.location.host + '/file.csv'; return getFile(url); }")
page.execute_script("window.getFile = function(url) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); xhr.send(null); return xhr.responseText; }")
data = page.evaluate_script("downloadCSVXHR()")
File.open(File.join(Rails.root, "tmp", "csv.data"), "w") { |f| f.write(data) }
end
Change the URL in the Javascript code to your CSV's location.
And finally, here's my step definition for validating the CSV file's contents:
And /^the contents of the downloaded csv should be:$/ do |contents|
file = File.open(File.join(Rails.root, "tmp", "csv.data"), "r")
file_contents = file.read
file_contents.chop!
file_contents.should == contents
end
Good luck. Hope this helps.

This is not currently possible with Poltergeist.
I think you'd be better off writing a test for this CSV which doesn't use Capybara. (E.g. by using the built-in Rails integration testing stuff and parsing the response as a CSV.)

There is an ticket to support downloading files in PhantomJS/Poltergeist and there are one or two forks which claims that they made it to work somehow. See https://github.com/ariya/phantomjs/issues/10052

Related

Remove file after Capybara test in Rails 7

In my Rails 7 app I've got Capybara test which checks if downloading a PDF file works. The issue is that after a successful check, Capybara saves this file in the main path of the project. How do I delete this file right after the test?
it 'download invoice' do
payment = build :isor_payment, :with_pdf
stub_payment(payment)
login_as user
visit payment_path(payment.platform_payment_id)
click_on 'Download'
expect(page).to have_content I18n.t('payments.main_data_component.invoice_pdf')
end
After that test it will save me a pdf named payment-43523452.pdf.
I'm assuming you're using Rspec as your test runner. You can configure an after(:each) callback to remove the file:
RSpec.configure do |config|
# all the rspec configuration options...
config.after(:each) do
FileUtils.rm('payment-43523452.pdf')
end
end
But this is a little brittle, you have to change the configuration whenever your download file name changes. I prefer to create a download directory under tmp/, like tmp/test_downloads/, then use the above callback method to remove all files from the tmp/test_downloads/ directory.
Configure the download file directory in the Capybara driver configuration, usually in spec_helper.rb. My driver configuration looks like this:
Capybara.register_driver :headless_chrome do |app|
chrome_options = Selenium::WebDriver::Chrome::Options.new
chrome_options.add_argument('--headless')
chrome_options.add_argument('--window-size=1400,800')
chrome_options.add_preference(:download,
directory_upgrade: true,
prompt_for_download: false,
default_directory: 'tmp/test_downloads/'
)
chrome_options.add_preference(:plugins, always_open_pdf_externally: true)
driver = Capybara::Selenium::Driver.new(app, :browser => :chrome, :capabilities => [chrome_options])
driver
end
Then your RSpec after(:each) callback will be:
FileUtils.rm_rf(Dir.glob("tmp/test_downloads/*"))

What can cause Capybara::Poltergeist::TimeoutError: in group testing that doesn't individually?

A Rails app that uses remote testing to test a CakePHP app running on a Vagrant Ubuntu VM.
My OS is macOS High Sierra.
'rspec/rails'
'capybara/rails'
'capybara/mechanize'
'capybara/poltergeist'
'phantomjs'
If I run rspec ./spec/features/my_tests_folder/,
the first 2 tests in the folder always pass and the rest always end up with Capybara::Poltergeist::TimeoutError:.
If I run any of the tests in that folder individually, they ALL pass ALWAYS.
There are 7 test files total. They each have 1 feature with 1 scenario. All are js: true.
I have tried increasing :timeout in Capybara.register_driver and increasing default_max_wait_time in Capybara.configure. Neither changed the outcome.
I've also played around with Capybara.reset! after and before each test. It didn't seem to matter, either.
When I ran this with config.order = :random, sometimes 5 out of 7 had the erros, sometimes only 2 out of 7. But, there were always some errors and some passing. Also, every test was in the errors group at least once.
I'm running out of ideas. What could cause something like this?
UPDATE (to include Capybara and Poltergeist configs and example of a failing test):
Configs in rails_helper.rb:
Capybara.register_driver :poltergeist do |app|
options = {
:timeout => 90, # default is 30
:js_errors => false,
:phantomjs => Phantomjs.path,
# :debug => true
}
Capybara::Poltergeist::Driver.new(app, options)
end
Capybara.register_driver :mechanize do |app|
driver = Capybara::Mechanize::Driver.new(app)
driver.configure do |agent|
agent.user_agent_alias = 'Mac Safari'
end
driver
end
Capybara.configure do |config|
config.run_server = false
config.app_host = "http://my_vm_domain.com"
config.javascript_driver = :poltergeist
config.default_driver = :mechanize
config.default_max_wait_time = 10 # default is 2
end
Example of failing test (not failing, but getting Capybara::Poltergeist::TimeoutError:):
require 'rails_helper'
feature 'agente visualiza estoques de um passeio', js: true do
scenario 'com sucesso' do
agente_log_in
visit '/roteiro/show/723'
find(:partial_href, 'new_passeio/723').click
select 'Passeio Teste', from: 'passeio_produto'
fill_in 'passeio_data', with: '11/11/2017'
within('button#estoque_controls_0') do
within('div#estoque_0_hora') do
expect(page).to have_content('07:30')
end
within('span#estoque_0_vagas') do
expect(page).to have_content('3')
end
end
within('button#estoque_controls_1') do
within('div#estoque_1_hora') do
expect(page).to have_content('10:00')
end
within('span#estoque_1_vagas') do
expect(page).to have_content('5')
end
end
end
end
Code from agente_log_in.rb in support folder:
def agente_log_in
Capybara.app_host = "http://my_vm_domain.com"
visit '/usuario/logout'
visit '/'
fill_in 'data[AdmUsuario][usuario]', with: 'agente'
fill_in 'data[AdmUsuario][senha]', with: 'pa$$w0rd'
click_on 'Entrar'
end
Code for that :partial_href find:
module Selectors
Capybara.add_selector(:partial_href) do
xpath {|href| XPath.descendant[XPath.attr(:href).contains(href)] }
end
end
Everything is fine with the other tests that are in the app's other folders. They're also fine if I run the tests in this folder individually. The problem only seems to happen when I run THIS specific folder as a whole.
After including the extra information requested by Thomas Walpole, I continued searching and studying possibilities.
I eventually came across poltergeist's issue #781 on GitHub, which describes a situation very similar to mine, and eventually presents a wait_for_ajax solution.
Before implementing it in my project, I read more about waiting for Ajax and found this post very helpful, too.
In the end, I chose jasonfb's code from GitHub because it seemed more thorough and informative.
It worked like a charm! My tests now pass consistently. I was even able to remove customization for :timeout and default_max_wait_time.
The CakePHP app in question is very js heavy and the specific part that this folder tests is particularly full of Ajax requests.

How do I force Capybara to download to the directory I specify and not the default download directory in Firefox?

When I run my test, Firefox downloads the file into the default download directory, USER\Downloads. However, I'm telling it to download into the PROJECT\tmp directory. How do I make it listen to me?
spec/config/capybara.rb
DOWNLOAD_DIRECTORY = Rails.root.join('tmp', 'capybara', 'downloads').to_s
Capybara.register_driver :firefox do |app|
profile = Selenium::WebDriver::Firefox::Profile.new
profile['devtools.selfxss.count'] = 9999
profile['browser.download.dir'] = DOWNLOAD_DIRECTORY
profile['browser.download.folderList'] = 2 # implies custom location
profile['browser.helperApps.neverAsk.saveToDisk'] =
'text/csv,text/tsv,text/xml,text/plain,application/pdf,application/doc,application/docx,image/jpeg,application/gzip,application/x-gzip'
profile.native_events = true
options = Selenium::WebDriver::Firefox::Options.new
options.profile = profile
Capybara::Selenium::Driver.new(
app,
browser: :firefox,
options: options,
desired_capabilities: Selenium::WebDriver::Remote::Capabilities.firefox(
marionette: true
)
)
end
part of the feature test
click_on 'CTD_chem_gene_ixn_types.csv'
# Only run for download capable drivers
download_capable do
wait_until(10) {
download_present?('CTD_chem_gene_ixn_types.csv')
}
end
And the error message
Failure/Error: File.exist?(Rails.root.join('tmp', 'capybara', 'downloads', file))
Timeout::Error:
execution expired
# ./spec/support/feature_support.rb:139:in `download_present?'
Capybara 2.16.1, selenium-webdriver 3.4.4, Firefox 59.0.2
I had to do the following
profile['browser.download.dir'] = DOWNLOAD_DIRECTORY
profile['browser.download.dir'] = DOWNLOAD_DIRECTORY.gsub('/', '\\') if Gem.win_platform?

Browser gem testing with capybara

I am using the gem Browser
to detect mobile devices and redirect them to a different landing on a rails app. The code I'm using is this one :
Rails.configuration.middleware.use Browser::Middleware do
redirect_to mobile_landing_path if browser.device.mobile? || browser.device.ipad? || browser.device.tablet?
end
Now I need to do a feature test using Rspec and capybara, and I'm a little lost here. Really appreciate the help .
Tests I have right now is using capybara user agent and looks like this: but it seems it stills gets the base landing and not the mobile one
feature 'Visiting the site from an' do
context 'Android device' do
background do
set_user_agent(:android)
end
scenario 'I should be redirected to mobile landing' do
visit root_path
expect(page).to have_current_path(mobile_landing_path)
end
end
end
The browser gem middleware will only be run when the HTTP_ACCEPT header is set and matches %r[(text/html|/)] . By default the rack-test driver doesn't set any headers. You can either overwrite the provided driver registration like
Capybara.register_driver :rack_test do |app|
Capybara::RackTest::Driver.new(app, :headers => { 'HTTP_ACCEPT' => 'text/html' })
end
or you can create a new driver registration
Capybara.register_driver :rack_test_with_html_accept do |app|
Capybara::RackTest::Driver.new(app, :headers => { 'HTTP_ACCEPT' => 'text/html' })
end
and then add driver: :rack_test_with_html_accept metadata to your tests that need that header to be set
feature 'Visiting the site from an', driver: :rack_test_with_html_accept do
...
end

Capybara, Poltergeist and Phantomjs and giving an empty response in body

I am getting am empty document back from phantomjs. I am trying to use Capybara and Poltergeist for setting up the phantomjs driver for Capybara.
I created a module as follows and included it in the file that uses needs to connect.
require 'capybara/poltergeist'
module Parser
module JSParser
include Capybara
# Create a new PhantomJS session in Capybara
def new_session
# Register PhantomJS (aka poltergeist) as the driver to use
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, :debug => true)
end
# Use XPath as the default selector for the find method
Capybara.default_selector = :xpath
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = :poltergeist
# Start up a new thread
#session = Capybara::Session.new(:poltergeist)
# Report using a particular user agent
#session.driver.headers = { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X)' }
# Return the driver's session
#session
end
# Returns the current session's page
def html
#session.html
end
end
end
Then, loading the page as follows:
class Loader
include Parser::JSParser
def load_page
new_session
visit "http://www.smashingmagazine.com"
#let phantomjs take its time
sleep 5
puts "html=#{html}"
end
end
Then, finally, calling the load_page
Loader.new.load_page
Here is the debug response from poltergeist
poltergeist [1364758785355] state default -> loading
{"response"=>true}
{"name"=>"visit", "args"=>["http://www.smashingmagazine.com"]}
poltergeist [1364758794574] state loading -> default
{"response"=>{"status"=>"success"}}
{"name"=>"body", "args"=>[]}
{"response"=>"<html><head></head><body></body></html>"}
As you can see, the response is just a blank document with only the html, head and body tags but nothing in the body tag.
What wrong am I doing? Observing network traffic, I am getting the full response back from the host (smashingmagazine.com in this case). Its after the response comes back that I don't know what is happening. Sometimes phantomjs is also crashing and on other occasions, it goes through with the empty body. Here is the last line that is printed on STDERR when phantomjs crashes
PhantomJS client died while processing {"name":"visit","args":["http://www.smashingmagazine.com"]}
I also had the similar issue. But the below option setting :phantomjs_options, helped me to solve the issue.
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app,
:phantomjs_options => ['--debug=no', '--load-images=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1'], :debug => false)
end
It sounds like a bug in PhantomJS when visiting this website. I suggest trying to load the website using only PhantomJS (not Poltergeist or Capybara) to see whether that works. If it also has trouble, report a bug against PhantomJS.
You can read about PhantomJS here: https://github.com/ariya/phantomjs/wiki/Quick-Start
Get html with this instead
puts "html=#{page.html}"

Resources