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('/')
Related
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
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
For a rails app I'm currently working on I recently changed the design somewhat so that the signout link no longer has the anchor text "Sign out" but instead a glyphicon from twitter bootstrap. the html for link now looks like this:
<a class="btn btn-primary btn-sm" data-method="delete" href="/users/sign_out" rel="nofollow">
<span class="glyphicon glyphicon-log-out"></span>
</a>
meanwhile my capybara test looks like this:
context "when not logged in" do
it 'cannot create wikis' do
#free_user = create(:user)
login_as(#free_user, :scope => :user)
click_link "Sign out"
visit root_path
expect(page).to_not have_link('Create wiki')
end
end
Now that the text "Sign out" is no longer there, I need a new way to identify the link. Checking the documentation for capybara (or rather this handy cheatsheet), it looks like I can supply either the text of the link or its id. So I tried giving it an id:
<a class="btn btn-primary btn-sm" data-method="delete" href="/users/sign_out" id="signout" rel="nofollow">
<span class="glyphicon glyphicon-log-out"></span>
</a>
So now it's got an id of "signout" however when I make this change to the test, it still won't pass.
1) Standard (free) User when not logged in cannot create wikis
Failure/Error: click_link "signout"
Capybara::ElementNotFound:
Unable to find link "signout"
# ./spec/features/standard_user_role_spec.rb:107:in `block (3 levels) in <top (required)>'
I tried making sure that I was still a logged in user in the test by creating and logging in the user as seen above and by adding a check that the html on the page contains Hello, since it says "Hello" and the name of the user when the user is logged in:
expect(page).to have_content('Hello')
This gave me another error that I don't understand:
1) Standard (free) User when not logged in cannot create wikis
Failure/Error: expect(page).to have_content('Hello')
Capybara::ElementNotFound:
Unable to find xpath "/html"
# ./spec/features/standard_user_role_spec.rb:107:in `block (3 levels) in <top (required)>'
So what could be going on here?
Full spec available here
You can re-add the text but hide it from all but screen readers:
<a class="btn btn-primary btn-sm" data-method="delete" href="/users/sign_out" id="signout" rel="nofollow">
<span class="sr-only">Sign out</span>
<span class="glyphicon glyphicon-log-out"></span>
</a>
This also improves accessibility somewhat.
context "when not logged in" do
it 'cannot create wikis' do
#free_user = create(:user)
login_as(#free_user, :scope => :user)
visit root_path
click_link "Sign out", visible: false
expect(page).to_not have_link('Create wiki')
end
end
Note that we explicitly tell Capybara to look for hidden text with the visible option.
For the applications, which use JS, that means the request is being sent using for example json format, and is begin triggered by click on the link. (suppose the HTML markup is the same). It is useful the following call:
find('Sign Out', visible: false).trigger('click')
or by id (if you'll add it):
find(:css, "#sign-out", visible: false).trigger('click')
With Rspec, I'm testing the presence of the logo in the navnar:
#spec/views/_header_spec.html.erb
require 'spec_helper'
describe "layouts/_header.html.erb" do
subject{rendered}
it "should have the clickable logo" do
render
should have_selector("img")
should have_link("/")
end
end
This is my generated HTML:
<a href="/" class="navbar-brand">
<img alt="Logo" src="/assets/logo.png">
</a>
The page is OK, but the test fails:
$rspec spec/views/_header_spec.rb
F
Failures:
1) layouts/_header.html.erb should have the clickable logo
Failure/Error: should have_link("/")
Capybara::ExpectationNotMet:
expected to find link "/" but there were no matches
# ./spec/views/_header_spec.rb:10:in `block (2 levels) in <top (required)>'
Finished in 0.13914 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/views/_header_spec.rb:7 # layouts/_header.html.erb should have the clickable logo
Randomized with seed 37707
The test fails, but the HTML behaviour page is correct, so I think my test is not working properly. Can you help me?
Replace
should have_link("/")
With
should have_link("Logo", href: "/")
have_link takes the display text of the link or alt attribute value of an image, and using the href option you can specify the corresponding path.
So I recently took on a project upgrading a Ruby 1.9.3 / Rails 3.2 application to Ruby 2 / Rails 4.0.2. All seems to be well, but I have a failing spec. I'm trying to test that a button is working at the bottom of the page for a "suggestions" box.
Failure message is:
Failures:
1) StaticPages Scheduler Page after authenticating filling in the suggestion form after submitting the form should send an email request with the form contents
Failure/Error: click_button("suggestion-button")
ActionController::UnknownFormat:
ActionController::UnknownFormat
# ./app/controllers/static_pages_controller.rb:39:in `suggestion'
# ./spec/features/static_pages_spec.rb:93:in `block (6 levels) in <top (required)>'
Spec reads:
context "after submitting the form" do
before(:each) do
click_button("suggestion-button")
end
Controller reads:
def suggestion
if user_signed_in?
user = current_user.email
else
user = "< not logged in >"
end
if params[:suggestion]
ContactMailer.suggestion_email(params[:suggestion], params[:pathname], user).deliver!
end
respond_to do |format|
format.json { render json: params[:suggestion] }
end
end
The source for the button on the page is:
<button type="submit" class="btn" id="suggestion-button">
Tell Us!
</button>
</form>
<script>
// After form submission, gray out and disable the form
$(document).ready(function(){
$("#footer-suggestion-form").bind("ajax:complete", function(event, xhr, status){
console.log($("#footer-suggestion-form input"));
$("#footer-suggestion-form input").attr("disabled","disabled");
$("#footer-suggestion-form button").attr("disabled","disabled").html("Thanks!");
console.log("did it!");
});
$("#suggestion-pathname").val(location.pathname);
});
</script>
Is it because it's an AJAX form? Or just a test that worked in Rails 3.2 but no longer works in 4 because of some change? I appreciate any help, I am truly lost.
I did try using :js => true in the spec and downloading the capybara-webkit gem, but when I try to run the spec it gets to that test and then just hangs there waiting for something to happen that never does.
EDIT: Progress. Now it gets to that test, sits for a big, and then fails with
1) StaticPages Scheduler Page after authenticating filling in the suggestion form after submitting the form should send an email request with the form contents
Failure/Error: before { visit scheduler_path }
RuntimeError:
Rack application timed out during boot
./spec/features/static_pages_spec.rb:31:in `block (3 levels) in