I have the following test:
expect(page).to have_selector('ul', count: 1)
expect(page).to have_selector('li', count: 21)
expect(page).to have_selector('li img', count: 21)
Basically I want to check that the list exists, that it has exactly 21 items and that each item has an image.
The problem with my code is that if the first element has 2 images, and the second has none, it would still pass.
How could I check every list item individually? Something like this...
expect(page).to have_selector('ul', count: 1)
expect(page).to have_selector('li', count: 21)
foreach('li') do |item|
expect(item).to have_selector('img', count: 1)
end
The answer by Florent B. is correct and tests what you want. To make the checks the way you were thinking you could do something like
expect(page).to have_selector('ul', count: 1)
lis = page.all('li', count: 21) # same effect as expectation since it will raise an exception if not 21 on page but gets access to the elements
lis.each do |li|
expect(li).to have_selector('img', count: 1)
end
however due to the number of queries that would perform the answer by Florent B. is going to be much quicker
I would simply check with an XPath that there's no li where the image count is not 1:
expect(page).to have_selector('ul', count: 1)
expect(page).to have_selector('ul li', count: 21)
expect(page).to have_selector('ul li img', count: 21)
expect(page).to have_no_xpath('//ul//li[count(.//img) != 1]')
Related
There are some articles (e.g. [1]) regarding solving flaky acceptance tests when using Capybara which advocates using e.g.
.to have_text("foo")
instead of
.to eql("foo")
In one of my tests I have .to match(/foo/) and every once in a while this fails. I assume that the match matcher is not in the same category as e.g. the have_text matcher and doesn't wait. The documentation doesn't mention anything regarding this.
Is there any regex matcher so that I can check e.g.
expect(next_url).to match(/foo/)
?
Versions used (not changeable):
capybara: 2.7.x
spec-rails: 3.6.x
[1] https://www.urbanbound.com/make/fix-flaky-feature-tests-by-using-capybaras-apis-properly
The docs for have_text link to the assert_text docs - https://www.rubydoc.info/gems/capybara/Capybara/Node/Matchers#assert_text-instance_method which show that it takes either a string
expect(page).to have_text('Something')
or a Regexp
expect(page).to have_text(/foo/)
As the article you linked to implies, if you find yourself using any non capybara provided matcher with information returned from Capybara you're probably doing something wrong, and setting yourself up for flaky tests.
If you have a page where elements have a delay appearing on the page, you can define a 'wait' method in 'capybara_helpers.rb'
def wait_for timeout = 10, &block
Timeout.timeout(timeout) do
loop do
condition = yield
if (condition)
break true
end
end
end
rescue Timeout::Error
raise "Condition not true in #{timeout} seconds"
end
After that, you can use 'wait_for' method like this:
wait_for { page.has_css?('.class', text: 'Something') }
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.
When I run the following command rspec spec/models/vote_spec.rb, I get the following error:
Failures:
1) Vote validations value validation only allows -1 or 1 as values
Failure/Error: expect ( #post.votes ).to eq(-1)
NoMethodError:
undefined method `votes' for nil:NilClass
# ./spec/models/vote_spec.rb:7:in `block (4 levels) in <top (required)>'
Finished in 0.00255 seconds (files took 2.37 seconds to load)
1 example, 1 failure
Here's my code for vote_spec.rb
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
expect ( #post.votes ).to eq(-1)
expect ( #post.votes ).to eq(1)
end
end
end
end
Sorry I am new to this, I guess my #post variable is not being set. Where should I look for this?
Correct. You're running into this error because your #post variable is nil. What do you mean by "where should I look for this?"
In order to fix this error, you need to define #post somehow in your spec, above the two "examples" in your it block. (This could go in the it block, or in a describe or let block above the it). Two options. Create the object long-form:
#post = Post.create(attribute_hash_here)
or use a factory of some sort (example below uses FactoryGirl):
#post = create(:post)
As it stands, however, were you to do that, your spec would still fail, because it has contrasting expectations:
expect ( #post.votes ).to eq(-1)
expect ( #post.votes ).to eq(1)
Unless the votes method on Post both returns a value AND alters that value, #post.votes will equal EITHER -1 or 1. So if it passes the first expectation, it will fail the second, and if it passes the second, it will fail the first.
** EDIT ** -- As ChrisBarthol pointed out, it's not clear why you need #post to exist at all. If you're just testing a vote's attribute validations, why not just test that object on its own?
First off these are model validations, and you are validating the vote model not the post model, so you should be setting #vote, and not #post. Secondly your test says you expect the value to equal -1 and then 1. How could it be both at the same time? Where are you setting the value such that you expect it? You have to restructure you tests so you are only testing one item at a time.
require 'rails_helper'
describe Vote do
let(:post) { Post.new(whatever post params) }
before { #vote=post.votes.build(whatever vote parameters you have) }
subject { #vote }
describe "validations" do
describe "+1 value valdiation" do
before { #vote.value = 1 }
it { should be_valid }
end
describe "-1 value valdiation" do
before { #vote.value = -1 }
it { should be_valid }
end
describe "other value valdiation" do
before { #vote.value = 0 }
it { should_not be_valid }
end
end
end
I'm guessing at your relationships. There are also better ways to write these tests but that should lead you down the right road.
I'm just curious on some good practices around testing for time in Rspec, ie. the datetime data type and the date data type. ON the shorthand, this is the problem i currently have, i'm trying to test the lsting page ie. index.html.erb but am looking for a past tense time method to use not future tense time method.
require 'spec_helper'
describe 'List Objects' do
it 'shows all objects' do
object11 = Object.create(
title: 'Local Stuff',
description: 'An article on local disputes',
posted_from: 'New Jersey',
posted_by: 'Ned Flanders',
posted_at: 6.days.from_now <-- Looking for a past tense time method to use.
)
visit objects_url
expect(page).to have_text(object1.title)
expect(page).to have_text(object1.description)
expect(page).to have_text(object1.posted_from)
expect(page).to have_text(object1.posted_by)
expect(page).to have_text(object1.posted_from)
expect(page).to have_text(object1.posted_at)
I believe you are looking for the #ago method:
object11 = Object.create(
title: 'Local Stuff',
description: 'An article on local disputes',
posted_from: 'New Jersey',
posted_by: 'Ned Flanders',
posted_at: 6.days.ago
)
The following works as expected:
within('h2', text: 'foo') do
should have_content 'bar'
end
I am trying to check within the parent element, using find(:xpath, '..')
Once you find an element, how to apply .find(:xpath, '..'), and then check for something within that element?
When you use XPath locator inside within it should start with . (if it doesn't start with . the search is done not within .myclass but within the whole document).
E.g.:
within('.myclass') do
find(:xpath, './div')
end
or:
find('.myclass').find(:xpath, './div')
Code from #BSeven's answer can be written in one line:
expect(find("//h2[text()='foo']/..")).to have_text('bar')
or
expect(page).to have_xpath("//h2[.='foo']/..", text: 'bar')
With the new syntax of Rspec after 2.11, it should be;
within('h2', text: 'foo') do |h|
expect(h).to have_content 'bar'
end
The following is one approach:
within('h2', text: 'foo') do
within(:xpath, '..') do
should have_content 'bar'
end
end