Rspec content vs text - ruby-on-rails

I've run across a couple examples where people test for the content of an html element with page.should have_selector "title", content: "my awsome content", but this seems to always pass. The correct call seems to be page.should have_selector "title", text: "my awsome content" (notice text: instead of content:).
What is the content: selector for in this Rspec call?

Short answer:
have_selector doesn't support a :content option.
Long answer:
Rspec creates dynamic methods that translates calls like [].should be_empty to [].empty?.should == true and, in your case, page.should have_selector "title", content: "my awesome content" to page.has_selector?("title, content: "my awesome content").should == true.
Capybara provides the has_selector? method, which basically just passes its parameters to its test/unit style assertion, assert_selector:
def has_selector?(*args)
assert_selector(*args)
rescue Capybara::ExpectationNotMet
return false
end
assert_selector then passes its parameters again to the Capybara finder method, all(), returning true if it found any matches and raising an exception if it doesn't (which has_selector? catches and then returns false with).
def assert_selector(*args)
synchronize do
result = all(*args)
result.matches_count? or raise Capybara::ExpectationNotMet, result.failure_message
end
return true
end
The all method returns all of the results that match the query. This is where we'll find the documentation on acceptable options:
# #overload all([kind], locator, options)
# #param [:css, :xpath] kind The type of selector
# #param [String] locator The selector
# #option options [String, Regexp] text Only find elements which contain this text or match this regexp
# #option options [Boolean] visible Only find elements that are visible on the page. Setting this to false
# (the default, unless Capybara.ignore_hidden_elements = true), finds
# invisible _and_ visible elements.
# #return [Array[Capybara::Element]] The found elements
We can see that content is not listed as a valid options, so is just ignored.

Related

rspec #variable returning nil

I have an issue with my #attributes variable. I would like it to be accessible to keep my code dry, but currently, I have to restate the variable and set it to "values" to get my rspec test to work. What is a better way to do this without duplicating the values.
ref: Unexpected nil variable in RSpec
Shows that it is not accessible in describe, but there needs be another solution. When would "specify" be appropriate? I have not used it.
describe "When one field is missing invalid " do
before(:each) do
#user = create(:user)
#attributes = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
end
values = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
values.keys.each do |f|
p = values.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(#user, p)
cr.valid?
end
end
end
Update based on comments:
I would also like to use the values array hash in other tests. If I put it in the loop as stated, I would still have to repeat it in other places. Any other recommendations?
Update: I tried using let(),
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
but get the following error.
attributes is not available on an example group (e.g. a describe or context block). It is only available from within individual examples (e.g. it blocks) or from constructs that run in the scope of an example (e.g. before, let, etc).
In either of your snippets, you don't need attributes inside of your specs. It is data to generate specs. As such, it must live one level above.
describe "When one field is missing" do
let(:user) { Factorybot.create(:user) }
attributes = { "has_car" => "true", "has_truck" => "true", "has_boat" => "true", "color" => "blue value", "size" => "large value" }
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
As you seem to have recognized, based on the other SO post you linked to, you can't refer to your instance variables out in your describe block. Just set it as a local variable as you've done.
Using let
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
## The variables are used INSIDE the it block.
it "returns invalid when a key is missing" do
attributes do |f|
p = attributes.except(f)
cr = CarRegistration::Vehicle.new(user, p)
expect(cr.valid?).to eq(true) # are you testing the expectation? Added this line.
end
end
end
Personally I don't like writing test (like the above) which could fail for multiple reasons. Sergio is correct. But if you want to use let you have to make use of it from WITHIN the it block - this example shows that.

RSpec: wait alternative to match

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') }

Testing index view sort order in RSpec feature spec (Capybara)

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

newbie can't define a method in ruby for cucumber test pass

I am trying to learn cucumber, here's an example code from a book:
class Output
def messages
#messages ||= []
end
def puts(message)
messages << message
end
end
def output
#output ||= Output.new
end
Given /^I am not yet playing$/ do
end
When /^I start a new game$/ do
game = Codebreaker::Game.new(output)
game.start
end
Then /^I should see "([^"]*)"$/ do |message|
output.messages.should include(message)
end
When I run this spec, I get this error:
Scenario: start game # features/codebreaker_starts_game.feature:7
Given I am not yet playing # features/step_definitions/codebreaker_steps.rb:15
When I start a new game # features/step_definitions/codebreaker_steps.rb:18
Then I should see "Welcome to Codebreaker!" # features/step_definitions/codebreaker_steps.rb:23
undefined method `messages' for #<RSpec::Matchers::BuiltIn::Output:0xa86a7a4> (NoMethodError)
./features/step_definitions/codebreaker_steps.rb:24:in `/^I should see "([^"]*)"$/'
features/codebreaker_starts_game.feature:10:in `Then I should see "Welcome to Codebreaker!"'
And I should see "Enter guess:" # features/step_definitions/codebreaker_steps.rb:23
See that it gives undefined method 'messages' error, yet it is defined in the Output class.
If I replace output.messages.should with Output.new.messages.should, it works fine. What is the problem here?
Edit: Probably output is a keyword, in new version of rails, when I changed it to outputz it worked fine. An explanation of this will be accepted as an answer.
Apparently, the output matcher has been added to rspec in version 3.0:
The output matcher provides a way to assert that the has emitted
content to either $stdout or $stderr.
With no arg, passes if the block outputs to_stdout or to_stderr. With
a string, passes if the blocks outputs that specific string to_stdout
or to_stderr. With a regexp or matcher, passes if the blocks outputs a
string to_stdout or to_stderr that matches.
Examples:
RSpec.describe "output.to_stdout matcher" do
specify { expect { print('foo') }.to output.to_stdout }
specify { expect { print('foo') }.to output('foo').to_stdout }
specify { expect { print('foo') }.to output(/foo/).to_stdout }
specify { expect { }.to_not output.to_stdout }
specify { expect { print('foo') }.to_not output('bar').to_stdout }
specify { expect { print('foo') }.to_not output(/bar/).to_stdout }

How to combine find and within using Capybara?

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

Resources