I'm trying to test my AJAX with Rspec/Capybara.
My page (recruiter#dashboard) contains 3 columns.
It loads in candidates that applied for a certain vacancy and their state.
Column 1 => state==pending
Column 2 => state==matched
Column 3 ==> state=="sealed"
In my spec I'm creating a vacancy with 1 applicant that has state pending.
print "Amount of vacancies => #{Vacancy.count} "
print "Amount of candidates => #{Employee.count} "
print "Amount of candidates applied for vacancy => #{Matching.where(vacancy_id: Vacancy.first.id).count}"
print "State of #{Employee.first.name} for #{Vacancy.first.name} => #{Matching.where(vacancy_id: Vacancy.first.id, employee_id: Employee.first.id).first.state}"
returns
Amount of vacancies => 1
Amount of candidates => 1
Amount of candidates applied for vacancy => 1
State of foo1 Hintz for vacancy1 => pending
So that would mean that this candidate should be loaded in the li below:
<ul id="applied-desktop-dashboard-ajax">
<li>
CANDIDATES
</li>
</ul>
Yet, when I run my test:
page.all("#applied-desktop-dashboard-ajax li").count.should eql(1)
returns
expected: 1
got: 0
When I
save_and_open_page
I see the li is empty.
So I tried
sleep(5)
after
visit "dashboard"
But no success.
Does anyone have an idea why my li aren't loading in this test (but are working on localhost just fine.)?
Full test:
require 'rails_helper'
RSpec.feature "Creating vacancies" do
before do
create(:matching)
end
scenario "Ajax testing" do
visit "/recruiters/sign_in"
fill_in "Email", with: "bedrijf1#hotmail.be"
fill_in "Password", with: "bedrijf1bedrijf1"
within 'form#new_recruiter' do
find('input[name="commit"]').click
end
expect(current_path).to eq '/'
visit "/dashboard"
sleep(5)
print "Amount of vacancies => #{Vacancy.count} "
print "Amount of candidates => #{Employee.count} "
print "Amount of candidates applied for vacancy => #{Matching.where(vacancy_id: Vacancy.first.id).count}"
print "State of #{Employee.first.name} for #{Vacancy.first.name} => #{Matching.where(vacancy_id: Vacancy.first.id, employee_id: Employee.first.id).first.state}"
# save_and_open_page
page.all("#applied-desktop-dashboard-ajax li").count.should eql(1)
end
end
When performing js tests the app being tested runs in a different thread than the test, which means they no longer share the same database connection - see https://github.com/jnicklas/capybara#transactions-and-database-setup. Because of this you need to disable transactional testing and use truncation or deletion to manage the database state. The easiest way to do that is to use database_cleaner and setup a config that will swap to the needed strategy for each test - https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example
Once you have that configured correctly then it's time to look at your test and make it less brittle with asynchronous testing.
expect(current_path).to eq '/' should be rewritten as expect(page).to have_current_path('/') so that Capybara will automatically wait for the page to change.
By default page.all doesn't wait for any elements to appear since 0 elements is a valid return. If you change page.all("#applied-desktop-dashboard-ajax li").count.should eql(1) to expect(page).to have_selector("#applied-desktop-dashboard-ajax li", count: 1) Capybara will wait for a little bit of time for the li to appear on the page rather than failing immediately because the ajax hadn't yet completed. You can also specify :minimum, :maximum, :between depending on what exactly you're verifying
Using the correct Capybara methods can remove the need for most if not all sleep()s in your testing.
Related
An index view of a Rails 4.2 app has a table with sort links at its header. If the user clicks the "E-mail" header, records are sorted by E-mail, and so on. When a sort link is clicked, the page is reloaded (with a query string like q=email+asc). AJAX is not yet used.
I've written the following test. It works, but I believe there should be a better way to test this.
it "sorts by e-mail when the 'E-mail' column header is clicked", :focus do
visit "/admin/users"
expected_id_order = User.order(:email).map(&:id)
# Once the page is reloaded (the new sort order is applied), the "stale"
# class will disappear. Note that due to Turbolinks, only the part of
# the DOM that is modified by a request will have its elements replaced.
page.execute_script("$('tr.user:first').addClass('stale')")
within "thead" do
click_link("E-mail")
end
# Force Capybara to wait until the page is reloaded
expect(page).to have_no_selector("tr.user.stale")
actual_id_order = page.body.scan(/<tr.+id="user_(.+)".*>/).flatten
expect(actual_id_order).to eq(expected_id_order)
end
Additional details:
<TR> elements have DOM IDs containing the DB IDs of their corresponding records, like <tr class="user" id="user_34">. Using regex to extract the order in which the records are displayed in the page is probably not the best solution. Can you suggest a better way?
I don't like the JavaScript hack (using JQuery to add the stale class and then waiting until it disappears to ensure the page was reloaded), but so far I could not find another way to ensure Capybara waits until the page is reloaded (the new sort order is applied). Can you suggest a better way?
The application uses Devise, so we need to create a user record in order to login. Note that the user created for login purposes inevitably appears in our test data.
Easiest way to do this is to use the fact that you control the test data, assign the strings accordingly and then use a regex to test the output a user would actually see rather than specific tags and ids.
within_table('user_table') do # any method that will scope to the table
expect(page).to have_text /user3#test\.com.+user2#test\.com.+user1#test\.com/ # initial expected ordering
click_link("E-mail")
expect(page).to have_text /user1#test\.com.+user2#test\.com.+user3#test\.com/ # sorted expected ordering
end
Capybara will wait, no JS hack needed, etc.
Taking ideas from this blog post you could do something like
# get the index of the first occurrence of each users' email address in the response body
actual_order = User.order(:email).map { |user| page.body.index(user.email) }
# should already be in sorted order
expect(actual_order).to eq(actual_order.sort)
As for having to wait for the .stale class to be removed, I'd suggest instead modifying your html to include some sort of "this is the currently sorted column" class on your th or a tags.
<a id="email-sort-link" class="sorted asc">E-Mail</a>
and then you could wait until you see a#email-sort-link.sorted. Don't usually like modifying code to facilitate testing, but this seems minor enough and allows you to style the link differently to remind the user the current sorting.
I had a specific case where I wanted to use shared examples. So I have created the following based on Thomas Walpole's answer.
def sort_order_regex(*sort_by_attributes)
/#{User.order(sort_by_attributes)
.map { |u| Regexp.quote(u.email) }
.join(".+")}/
end
RSpec.shared_examples "sort link" do |link_text:, sort_by:|
# Make sure your factory creates unique values for all sortable attributes
before(:each) { FactoryGirl.create_list(:user, 3) }
it "sorts by #{sort_by} when the #{link_text} link is clicked" do
visit "/admin/users"
# Check the record's order by matching the order of their e-mails (unique).
initial_order = sort_order_regex(:first_name, :last_name)
tested_order = sort_order_regex(sort_by)
within_table "users_table" do
expect(page).to have_text(initial_order)
click_link(link_text, exact: false)
expect(page).to have_text(tested_order)
end
end
end
describe "User CRUD", type: :feature, js: true do
describe "sorting" do
include_examples "sort link", link_text: "E-mail", sort_by: :email
include_examples "sort link", link_text: "Role", sort_by: :role
include_examples "sort link", link_text: "Country", sort_by: :country
include_examples "sort link", link_text: "Quotes", sort_by: :quotes_count
end
#...
end
We can test it with CSS stuff.
An example:
|---------------------|------------------|
| username | email |
|---------------------|------------------|
| bot1 | bot1#mail.com |
|---------------------|------------------|
| bot2 | bot2#mail.com |
|---------------------|------------------|
Our RSpec can be like:
RSpec.describe "returns expected sorted row data" do
it 'returns expected sorted row data' do
# the headers column name
within 'tr:nth-child(1)' do
expect(page).to have_text 'username password'
end
# the second row
within 'tr:nth-child(2)' do
expect(page).to have_text 'bot1 bot1#mail.com'
end
# the third row
within 'tr:nth-child(3)' do
expect(page).to have_text 'bot2 bot2#mail.com'
end
end
end
I am using Rails 4 and Ruby 2.2 with RSPEC, Capybara and FactoryGirl.
I am trying to test that a user can write a story adding characters (other users). It works in the browser, but when I run the tests, I get the following message, indicating that the title is missing:
Failure/Error: expect(page).to have_content("Thank you for sharing a story.")
expected to find text "Thank you for sharing a story." in "Family Matters Write New Story Add User Log Out * Title DescriptionSo this is what happened that night in 1972 Who was a part of this story?Loni Cabral Fernando Cabral Cover image"
When I add save_and_open_page, I can see that the title has been inserted. When I remove the lines to select characters, the tests pass.
Here is the test file:
require 'rails_helper'
require_relative '../support/new_story_form'
feature 'create story' do
let(:new_story_form) { NewStoryForm.new}
let(:user) { FactoryGirl.create(:active_user) }
let(:character1) { FactoryGirl.create(:active_user, first_name:"Loni", last_name:"Cabral") }
let(:character2) { FactoryGirl.create(:active_user, first_name:"Fernando", last_name:"Cabral") }
before do
login(user)
user.add_relative(character1, "Child")
user.add_relative(character2, "Child")
end
scenario 'create new story with valid data' do
new_story_form.visit_form.fill_in_with(
title: "Great story",
cover_image: "cover_image.png"
).submit
expect(page).to have_content("Thank you for sharing a story.")
expect(page).to have_content("Who was involved:")
expect(page).to have_content(character1.name)
expect(page).to have_content(character2.name)
expect(Story.last.cover_image_identifier).to eq("cover_image.png")
expect(Story.last.title).to eq("Great story")
expect(Story.last.user).to eq(user)
expect(Story.last.participants.first).to eq(character1)
end
scenario 'cannot create story with invalid data' do
new_story_form.visit_form.submit
expect(page).to have_content(" can't be blank")
end
end
And here is the new_story_form support file:
class NewStoryForm
include Capybara::DSL
def visit_form
visit('/')
click_on("Write New Story")
self
end
def fill_in_with(params = {})
fill_in("Title", with: params.fetch(:title, "Great story"))
fill_in("Description", with: "So this is what happened that night in 1972")
attach_file('Cover image', "#{Rails.root}/spec/fixtures/" + params.fetch(:cover_image, "cover_image.png"))
select("Loni Cabral", from:"Who was a part of this story?")
select("Fernando Cabral", from:"Who was a part of this story?")
self
end
def submit
click_on("Create Story")
end
end
Edit
After much debugging, I realized that the tests are failing because of a validation that is not working correctly.
It looks like your submit is failing and so it's redirecting back to the edit page, and not showing the "Thank you for sharing a story." text - Since you say it works if you don't select the characters I would guess it's some kind of nested attributes error, but your test log should explain exactly why the submit is failing. The reason the title isn't shown in the text searched is because the value of an <input type="text"/> element (which is being shown because it's redirecting to the edit page) is not part of the text content of a page
I have this feature:
feature "Blog features -", type: :feature, js: true do
let!(:admin) { FactoryGirl.create(:admin) }
scenario "Create new Blog" do
expect do
sign_in_as_admin(admin)
visit "/admin/blogs/new"
fill_in "blog_title", with: "title"
fill_in "blog_content", with: "lorem ipsum dolor"
click_button "Save"
end.to change(Blog, :count).by(1)
end
end
The Blog is saving correctly on the database but the test is not passing and I get this error: expected #count to have changed by 1, but was changed by 0
I don't know enough about your setup to determine if it's an error in the upstream code or if it's simply a race condition. My gut tells me you're racing. Try adding a sleep after the click save to see if that helps.
If your test is booting up a second process for your server (which I suspect it is) then your test is firing off a request (to be handled by the server at some future time) and then immediately checking the blog count. The request hasn't been handled by your server by the time you're checking the blog count.
Instead of checking the blog count at the database level, I'd recommend checking text or elements on the page. The user is getting some feedback from the save right? Assert against that.
I had the same problem as Nick Rutten in this thread Rails Tutorial: RSpec test decoupling. I suspected the problem was the FactoryGirl.create() saving to the database, and thanks to #prusswan I fixed it.
However I do not understand why these puts don't indicate different User.count.
This is the snippet I'm interested in:
describe "with valid information" do
puts "Number of users pre-Factory:" + User.count.to_s
let(:user){FactoryGirl.create(:user)}
puts "Number of users post-Factory:" + User.count.to_s
(...)
end
I get:
Number of users pre-Factory:0
Number of users post-Factory:0
But shouldn't I get instead?:
Number of users pre-Factory:0
Number of users post-Factory:1
Btw, the factory is defined (although not so relevant now) as:
FactoryGirl.define do
factory :user do
name "Example User"
email "user#example.com"
password "foobar"
password_confirmation "foobar"
end
end
I'm stuck trying to understand why the counter is not increasing, wasn't the problem that it did increase to start with?
thank you
Edit using #derekyau's suggestions:
Right now I've got in this order:
it "print counter before let"
puts "Number of users:" + User.count.to_s
end
let(:user){FactoryGirl.create(:user)}
it "print counter after let"
puts "Number of users:" + User.count.to_s
end
However now I get both values 1:
Number of users:1
Number of users:1
Close but not there yet!
Edit 2
As explained by #Peter Alfvin here: Within a given RSpec describe block, does the order of let, before and it statements matter? . The order of let, before, it is pretty much established, regardless of the code. That, together with #derekyau's explanation closes the question.
Good question, I think that let(:user) is lazily evaluated so it isn't actually called until you use it inside of an it block.
If you want to force it to be evaluated try let!
let!(:user) {FactoryGirl.create(:user)}
If you want to see it change try doing the puts inside of an "it block"
it "check user count" do
puts User.count
end
You should see '1'
I am new to cucumber and I would like to ask for a suggestion for how I can organize the following step definition,
Feature: Manage events
In order to Manage events
Organizer
wants to create new events
Background:
Given I am logged in as organizer
Scenario: Publish new event
Given I am on the new event page
When I fill in "Title" with "Rails event"
And I fill in "Start at" with "2012-5-4"
And I fill in "End at" with "2012-5-5"
And I fill in "Description" with "Bla bla"
And I check "Published"
And I press "Create Event"
Then I should see "Rails event"
Here are the step definitions that create the ambiguity,
When /^I fill in "Start at" with "(.*?)-(.*?)-(.*?)"$/ do |year, month, day|
enter_date(:event_start_at, year, month, day)
end
When /^I fill in "End at" with "(.*?)-(.*?)-(.*?)"$/ do |year, month, day|
enter_date(:event_end_at, year, month, day)
end
When /^I fill in "(.*?)" with "(.*?)"$/ do |name, value|
fill_in name, :with => value
end
private
def enter_date(id, year, month, day)
select year, :with => "#{id}_1i"
select month, :with => "#{id}_2i"
select day, :with => "#{id}_3i"
end
What happens is that the first 2 definition is ambiguous with the last definition. But for start and end date, I have to handle them differently.
What I know is that cucumber has the --guess option which solves that issue. But is this the best practice?
Why not just do something like:
When /^I fill in "([^"]*)" with "([^"]*)"$/ do |name, value|
case(name)
when "Start at" then
parts = value.split(/-/)
enter_date(:event_start_at, parts[0], parts[1], parts[2])
when "End at" then
parts = value.split(/-/)
enter_date(:event_end_at, parts[0], parts[1], parts[2])
else
file_in name, :with => value
end
end
(Or, something along those lines, typed into the textbox so not sure if that will run correctly as is.)
Get rid of the quotes around "Start at" and "End at". They will no longer match your last step. I've found that quotes cause a lot of ambiguity, and so I generally only use them for things like filenames or paths that have spaces in them. When in doubt, I find that removing quotes is the best practice as it removes a lot of guesswork for Cucumber.