Integration test with Rails, Capybara with React Component - ruby-on-rails

I've created a little scaffold for create posts with Rails, but instead use the standard form with erb I've created a simple React Component (I use the gem react-rails and browserify-rails).
You can find a sample repo with all the source code here!
The React component looks like this:
class NewPostForm extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return(
<form action="/posts" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓"/>
<input type="hidden" name="authenticity_token" value={this.props.authenticityToken}/>
<div className="field">
<label for="post_title">Title</label>
<input type="text" name="post[title]" id="post_title" onChange={(e) => this.setState({title: e.target.value})} />
</div>
<div className="field">
<label for="post_body">Body</label>
<textarea name="post[body]" id="post_body" onChange={(e) => this.setState({body: e.target.value})}></textarea>
</div>
{ this.state.title &&
<div class="actions">
<input type="submit" name="commit" value="Create Post" data-disable-with="Create Post"/>
</div>
}
</form>
)
}
}
Note that the submit button is added to the DOM only if the title input field is filled.
The file app/views/posts/_form.html.erb is this:
<%= react_component 'NewPostForm', {authenticityToken: form_authenticity_token.to_s}, {prerender: true} %>
Now I've created this integration test that use Capybara:
require 'test_helper'
class PostCreationTest < ActionDispatch::IntegrationTest
test "post creation" do
visit new_post_path
fill_in 'post[title]', with: 'Big News'
fill_in 'post[body]', with: 'Superman is dead!'
click_button 'Create Post' # <=== DO NOT FIND THIS BUTTON CAUSE IS ADDED WITH REACT
assert page.has_content?('Post was successfully created.')
end
end
The test fail with this error:
Run options: --seed 21811
# Running:
E
Error:
PostCreationTest#test_post_creation:
Capybara::ElementNotFound: Unable to find visible button "Create Post"
test/integration/post_creation_test.rb:9:in `block in <class:PostCreationTest>'
bin/rails test test/integration/post_creation_test.rb:5
In test_helper.rb I've configured Capybara to use poltergeist driver.
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, {
js_errors: false,
phantomjs_options: ['--ignore-ssl-errors=yes', '--ssl-protocol=any'],
debug: false,
timeout: 500,
phantomjs: File.absolute_path(Phantomjs.path)
})
end
Capybara.javascript_driver = :poltergeist
Capybara.server_port = 3001
If I display always the submit buttom (removing { this.state.title &&) in the react component the test pass successfully.
So, there is a way to make this test works with this setup and React components?

You have a couple of issues here. First is that you're not actually running your test with a JS capable driver. You have configured poltergeist for use with Capybara, but you've never told Capybara to use if for the specific test you want it used by setting Capybara.current_driver - see https://github.com/teamcapybara/capybara#using-capybara-with-minitest
Adding Capybara.current_driver = Capybara.javascript_driver for the failing test brings up the next issue you have.
PhantomJS (used by Poltergeist) doesn't currently support ES5.1+ JS or modern CSS so you need to polyfill and transpile back to ES5 levels if you want to use it. This becomes immediately clear if you stop hiding the JS errors in your Poltergeist config (js_errors: true). The initial error because of this is that your code uses the Map class which wasn't added until ES6/2015 so it's not supported by Poltergeist (without polyfill).
As a beginner with Capybara JS testing you're probably better off starting your JS requiring tests using selenium (so you can see exactly what's happening and have support for modern JS/CSS), and then when the tests are working possibly staying with selenium and headless chrome or headless FF.
Capybara.javascript_driver = :selenium # or :selenium_chrome

Related

Rails/Capybara: How to make attach_file work with active storage direct upload?

I am having trouble to attach files to inputs which use direct upload inside my system tests (Capybara). All tests worked before I switched to direct upload. I have also tried to manually submit appropriate forms via Browser and everything works there. Unfortunately, no luck with Capybara :/.
Inside view, I have following input:
<%= f.input :desktop_files, as: :file, input_html: { direct_upload: true, multiple: true } %>
and file is attached to input in system test by:
attach_file 'uploads_create_assets_former[desktop_files][]', "#{fixture_path}/files/image.jpg"
When I try to run test which uses similar piece of code, I get:
Selenium::WebDriver::Error::UnexpectedAlertOpenError: unexpected alert open: {Alert text : Error reading image.jpg}
(Session info: headless chrome=94.0.4606.81)
and when I check console inside browser opened by Capabyra, I can see following error:
FileReader error
My suspicion is that Capabyra/Selenium has problem to access attached file, but I don't know about any other way how to assign file to input. Maybe there is some Capybara magic which comes to play here :) -- hopefully, I am not only one who uses Rails direct upload and needs to test this piece of code with system tests...
I am using:
ruby (3.0.0)
rails (6.1.4.1)
selenium-webdriver (4.0.3)
capybara (3.35.3)
webdrivers (4.7.0)
and for capybara:
Capybara.register_driver :headless_chrome do |app|
options = Selenium::WebDriver::Chrome::Options.new(
args: %w[headless disable-gpu no-sandbox window-size=1440x768]
)
options.add_preference(:download, prompt_for_download: false,
default_directory: Rails.root.join('tmp/downloads').to_s)
options.add_preference(:browser, set_download_behavior: { behavior: 'allow' })
Capybara::Selenium::Driver.new(app, browser: :chrome, capabilities: options)
end
Edit:
Html code of form which should do upload looks like this:
<form class="formtastic uploads_create_assets_former" id="new_uploads_create_assets_former" enctype="multipart/form-data" action="/admin/upload/create" accept-charset="UTF-8" method="post">
<fieldset class="inputs">
<ol>
<li class="file input optional" id="uploads_create_assets_former_desktop_files_input"><label for="uploads_create_assets_former_desktop_files" class="label">Dateien (Computer)</label>
<input id="uploads_create_assets_former_desktop_files" multiple="multiple" data-direct-upload-url="http://127.0.0.1:49538/rails/active_storage/direct_uploads" type="file" name="uploads_create_assets_former[desktop_files][]" />
</li>
</ol>
</fieldset>
<fieldset class="actions">
<ol>
<li class="action input_action " id="uploads_create_assets_former_submit_action">
<input type="submit" name="commit" value="Nächster Schritt" data-disable-with="Nächster Schritt" />
</li>
</ol>
</fieldset>
</form>
I have not deviated in any way from Active Storage direct upload documented at https://edgeguides.rubyonrails.org/active_storage_overview.html#direct-uploads. Upload of files starts on former submission.
Another edit:
I have prepared minimalistic Rails app where you can try to play with my issue: https://github.com/martintomas/capybara-direct-upload. I have double checked that path is correct (otherwise Capybara::FileNotFound is raised), tried relative and absolute paths. I have also checked that anybody can read file:
-rw-r--r-- 1 martintomas staff 26436 Oct 22 12:51 image.jpg
Same problem happens when tests are run on my local machine or inside CI environment. To be honest, I have run out of ideas so I have decided to go for hacky solution now.
Hacky solution:
If you absolute trust active storage direct upload implementation and you don't have extra js code related to direct upload, you can turn it off inside system tests.
def attach_file(locator = nil, paths, make_visible: nil, **options)
turn_off_direct_upload # Capybara does not work with direct upload
super
end
def turn_off_direct_upload
page.execute_script 'document.querySelectorAll("input[data-direct-upload-url]:not([data-direct-upload-url=\"\"])").forEach((input) => { delete input.dataset.directUploadUrl } )'
end

Erratic behavior of Vue.js application in test mode (rspec)

I have a Rails 6 application which I test with rspec, Capybara and Chrome headless on a remote VM. With the new webdrivers gem, not that ancient poltergeist thing.
It has an user manager mini-app written in Vue 2.something that behaves in some stupefying ways:
Excerpt from Vue application
{
el: "#app",
data: {
initial_load_completed: false,
users: []
},
created: function(){
this.loadUsers();
},
methods: {
loadUsers: function(){ /* straightforward JSON load from server into .users and set .initial_load_completed to true */ }
/* lots of other code */
},
computed: {
hasUsers: function(){
return this.users.length > 0;
}
/* lots of other code */
}
}
View excerpt
<div id="app">
<!-- loads of other code -->
<div v-if="!initial_load_completed && !hasUsers">Loading your users, please wait...</div>
<div v-if="initial_load_completed && !hasUsers">There are no users for your account right now...</div>
<!-- lots of other code -->
</div>
The application works perfectly in prod and dev, on chrome, safari, tablets, iphones, even on my 3 year old smart TV Trashroid, even on IE. But under rspec tests it does things such as this:
This example with those 2 divs showing/hiding based on users loaded is just a small thing that's wrong in this picture, many other controls were supposed to not show with an empty users array. And this is a happy happy joy joy case, about 50% of example runs it just doesn't output anything at all, #app is blank... randomly.
In my test.log I see how the Vue app hits the JSON endpoint of my back-end and how it renders data with a 200.
For the life of me I can't imagine how initial_load_completed can be true and false at the same time.
What I've tried?
Rebooted the machine (heh). Then reinstalled all software to latest versions.
Then spent about 2 days trying to get chrome to work on a "virtual" display to which I would connect to see what's going on... after some 218 iterations fixing various deps/errors and configurations and code and signs and more errors and so on I just gave up.
Driver definition:
Webdrivers.logger.level = :DEBUG
default_chrome_args = [ '--disable-extensions', '--no-sandbox', '--disable-dev-shm-usage', '--remote-debugging-port=9222', '--remote-debugging-address=0.0.0.0' ]
Capybara.register_driver :headless_chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( loggingPrefs: { browser: 'ALL' }, chromeOptions: {
'args' => default_chrome_args + ['--headless', '--disable-gpu', '--window-size=1920,1600' ]
})
Capybara::Selenium::Driver.new app, browser: :chrome, desired_capabilities: capabilities
end
CSP's are disabled, tried with and without them anyway.
Yesterday I tried logging JS errors:
config.after(:each, type: [ :feature, :system ], js: true) do
errors = page.driver.browser.manage.logs.get(:browser)
if errors.present?
aggregate_failures 'javascript errrors' do
errors.each do |error|
expect(error.level).not_to eq('SEVERE'), error.message
next unless error.level == 'WARNING'
STDERR.puts 'WARN: javascript warning'
STDERR.puts error.message
end
end
end
end
... no luck.
config.after(:each, type: [ :feature, :system ], js: true) do
errors = page.driver.browser.manage.logs.get(:browser)
errors.each do |error|
STDERR.puts error.message
end
end
... also nada just like several other few variations of this code.
Can't even seem to get the examples to "puts :whatever" to stdout but that's another story.
Can someone kind at heart pretty please help a poor dumb me not lose all hair?
Something that is not clear from the code samples in your question, is whether you are actually applying the driver you are defining.
System tests will use the default driver, so you must set it explicitly:
RSpec.configure do |config|
config.before(:each, type: :system) {
driven_by :headless_chrome
}
end
It can also be applied on a per-scenario basis on feature tests:
RSpec.feature 'Balance' do
scenario 'check the balance', driver: :headless_chrome do
...
end
end

Unable to find field “q” (Capybara::ElementNotFound) via fill_in

Trying to get the "what" form (aka job title), from indeed.com
Error when trying to run the program:
/var/lib/gems/2.3.0/gems/capybara-2.11.0/lib/capybara/node/finders.rb:44:in `block in find': Unable to find field "q" (Capybara::ElementNotFound)
Inspecting element via firefox from indeed.com yields: name="q"
<span class="inwrap">
<input class="input_text" maxlength="512" size="31" aria-labelledby="what_label_top hidden_colon what_label_bot" name="q" autocomplete="off" id="what">
</span>
<div style="width:250px"><!-- --></div>
Which matches the code in the scraper:
def perform_search
# For indeed
fill_in 'q', :with => #skillset
fill_in 'l', :with => #region
find('#fj').click
sleep(1)
end
The entire code can be found at:
https://github.com/jasnow/job-hunter/blob/master/scraper.rb
Now the problem here is the inability to locate name="q" Are there any other ways I could link to that form on indeed.com so I could initiate webscraping? I'm talking xpath or css perhaps.
Your code only allows the URL http://www.indeed.com , but that URL redirects to https://www.indeed.com and also hits http://indeed.com. Therefore your page load is being blocked. Change to config.allow_url("indeed.com") and it should be able to find the input.

rails rspec capybara select not working

In my rails app, I have a select on the navigation bar as follows:
<body>
<div id="wrapper">
<!-- Navigation -->
<nav role="navigation" style="margin-bottom: 0">
<div class="navbar-default sidebar hidden-sm hidden-xs" role="navigation">
<div class="sidebar-nav">
<ul class="nav" id="side-menu">
<li>
<h4 class="sidebar-title">SGPLAN</h4>
</li>
<li class="logo">
<form action="#">
<select name="" id="change_plan" class="form-control plan">
<option value="1" id="plan_id" selected="">first </option>
<option value="2" id="plan_id">other </option>
</select>
</form>
</li>
and javascript in application.js to load the home page when the user selects a different option.
$(document).ready(function() {
//...
$('#change_plan').on('change', function(){
var str = ''
str += $( this ).val() + " ";
setCookie("plan", str,1);
window.location.href = "/";
})
});
I have written the following test for this feature using rspec, capybara and capybara-webkit:
require 'rails_helper'
feature "Change plan", :js do
background do
login_as create(:admin_user), scope: :user
Agency.current = Agency.find_by(initials: 'SECTI').id
FactoryGirl.create(:other_plan)
Plan.current = Plan.find_by(name: 'first').id
end
scenario "User changes the current plan" do
visit "/milestones"
save_and_open_page
select('other', from: 'change_plan')
# within '#change_plan' do
# find("option[value='2']").click
# end
# find('#change_plan').find('option', text: 'other').select_option
expect(current_path).to eq("/")
end
end
save_and_open_page results in the html snippet as shown above.
The result of running the test is as follows:
Failures:
1) Change plan User changes the current plan
Failure/Error: expect(current_path).to eq("/")
expected: "/"
got: "/milestones"
(compared using ==)
# ./spec/features/plans/change_plan_spec.rb:19:in `block (2 levels) in <top (required)>'
Finished in 1 minute 1.71 seconds (files took 2.04 seconds to load)
1 example, 1 failure
If I use find('#change_plan')... or find("option...") (as per the commented out lines) in the test instead of the select, the result is the same.
My versions are follows (from Gemfile.lock):
capybara (2.7.1)
capybara-webkit (1.11.1)
database_cleaner (1.5.3)
factory_girl_rails (4.7.0)
rails (4.2.5)
rspec-core (3.5.4)
rspec-expectations (3.5.0)
rspec-mocks (3.5.0)
rspec-rails (3.5.2)
and ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
What do I need to do get this test to work? Should I be using a different test platform? We are relatively committed to rspec but less so to capybara.
Update
I finally got this working with the help of Thomas and employing multiple suggestions that he provided.
There was a javascript error with the capybara webkit driver.
I tried the selenium driver but got a 503 error at the visit /milestones step.
I then switched to the poltergeist driver and found that the wait behaviour was also an issue - so I had to use have_current_path.
You shouldn't be using the eq matcher with current_path since it has no waiting/retrying behavior. This means you're checking the path before the page has time to change. Instead use the have_current_path matcher provided by Capybara
expect(page).to have_current_path('/')

Capybara::ElementNotFound: Unable to find field "Key name"

I've read the similar questions to this but they don't solve my issue. I'm new with js testing so I think I might be doing something wrong.
The form produces this HTML
<form class="new_category_item_key" id="new_category_item_key" action="/guides/dungeon-boss/categories?category_id=heroes" accept-charset="UTF-8" data-remote="true" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="b7wiF07zYh/Nl727M3y0Uk1TbroMJFuGqTK6fYNlNted/5G4Wmz4BZLb7IazzyP5md/wWRb1D28ePhrzt2uMSA==" />
<label for="category_item_key_name">Key name</label>
<input type="text" name="category_item_key[name]" id="category_item_key_name" />
<select name="category_item_key[key_type]" id="category_item_key_key_type"><option value="1">Value</option>
<option value="2">Text</option>
<option value="3">Image</option></select>
<input type="submit" name="commit" value="Add New Key" />
</form>
and I have the following integration test on the form
setup do
#user = users(:michael)
#user1 = users(:archer)
#guide = Guide.find(1)
#mod_relationship = game_mods_relationships(:mod1)
#category = Category.find(1)
Capybara.current_driver = Capybara.javascript_driver # :selenium by default
end
test "adding keys mod success then fail" do
log_in_as(#user)
get edit_guide_category_path(#guide, #category)
assert_template 'categories/edit'
assert_difference 'CategoryItemKey.count', 1 do
fill_in 'Key name', with: "diablo"
click_button "commit"
end
end
when I run the test I get the following error
Capybara::ElementNotFound: Capybara::ElementNotFound: Unable to find field "Key name"
Looking at the HTML I can see the field is there. If I try using the inputs id it still fails, if I remove the fill in line then it says it cant find the button to click which is also there. I assume its getting the right page because get edit_guide_category_path(#guide, #category) works for the other tests (but they are non js tests and don't use selenium).
Its probably something simple but I cant get it.
You're mixing up two different libraries -- You can't use get with Capybara, you use visit(url) to go to the page. You also shouldn't normally be asserting templates in a feature test, thats for lower level tests.

Resources