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
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') }
Goal: Trying to submit empty register form fails due to validation errors. Expects are present in scenario as single line (method). Errors are being checked very precisely in terms of their placement. I don't want to use classical counting until that's the only way left.
def cant_be_blank_error_is_displayed(number_of_times = 1)
expect(page).to have_content t('errors.messages.blank'), count: number_of_times
end
require 'rails_helper'
require 'email_helper'
feature 'register organisation', type: :feature, js: true do
let!(:register_page) { RegisterPage.new }
let!(:login_page) { LoginPage.new }
let!(:organisation) { create :organisation, name: 'organisation_name' }
let!(:owner) { create :user, email: 'user#example.com', role: :owner }
let(:form) { register_page.register_form }
before { register_page.load }
context 'creation fails because' do
scenario 'mandatory fields were left empty' do
register_page.submit_empty_form
all_mandatory_fields_cant_be_blank_error_displayed
# expect(form).to have_content mail_blank_or_taken_error
end
scenario (...)
end
end
Method:
1. Define specific errors as elements in the page
class RegisterPage < SitePrism::Page
set_url "/sign_up"
section :register_form, :xpath, "//*[#id='new_user']" do
element :email, :xpath, "//*[#id='user_email']"
(...) # other input fields
# binding.pry confirms they appear on the page
element :mail_blank_or_taken_error, :xpath, "//*[#id='new_user']/div[2]/span"
element :blank_password_error, :xpath, "//*[#id='new_user']/div[4]/span"
element :password_too_short_error, :xpath, "//*[#id='new_user']/div[2]/div[3]/p"
element :mismatch_error, :xpath, "//*[#id='new_user']/div[2]/div[4]/span"
element :blank_name_error, :xpath, "//*[#id='new_user']/div[2]/div[5]/span"
element :name_taken_error, :xpath, "//*[#id='new_user']/div[2]/div[5]"
element :wrong_format_error, :xpath, "//*[#id='new_user']/div[7]/span
(...)
end
end
2. Create error_alerts.rb helper to store expects in one method
def all_mandatory_fields_cant_be_blank_error_displayed
[
mail_blank_or_taken_error,
blank_password_error,
signs_limit_error,
blank_name_error,
blank_adress_line_error,
blank_city_error,
blank_country_error,
blank_zipcode_error,
blank_phone_number_error,
blank_website_error,
].each do |validation_errors|
expect(form).to have_content validation_errors
end
end
Obstacle:
Errors are default rails ones, so they are not defined as translations, which I used in another spec in the same convention and everything worked flawlessly.
example:
def invalid_email_or_password_error_displayed
expect(login_page).to have_content t("devise.failure.invalid", authentication_keys: "Email")
end
Failures: I get following errors.
1# my way - same issue occurs for each object from the |validation_errors|
1) register organisation creation fails because mandatory fields were left empty
Failure/Error: mail_blank_or_taken_error,
NameError:
undefined local variable or method `mail_blank_or_taken_error' for #<RSpec::ExampleGroups::RegisterOrganisation::CreationFailsBecause:0x007fe56b336370>
# ./spec/support/error_alerts.rb:63:in `all_mandatory_fields_cant_be_blank_error_displayed'
# ./spec/features/organisation_registration_spec.rb:16:in `block (3 levels) in <top (required)>'
2# classical expect
1) register organisation creation fails because mandatory fields were left empty
Failure/Error: expect(form).to have_content
mail_blank_or_taken_error
NameError:
undefined local variable or method `mail_blank_or_taken_error' for # <RSpec::ExampleGroups::RegisterOrganisation: :CreationFailsBecause:0x007ff02e7b9348>#./spec/features/organisation_registration_spec.rb:17:in `block (3 levels) in <top (required)>'
Question: Is my way technically achievable after some modifications or the only solution is to use classical counting method? Can loop iterate through page's elements? Here's an example of its similiar use, though expects are strings and the loop itself is included in spec's scenario, instead of being exported to external method in a helper.
scenario "is successful" do
all_ministry_memberships_page.create_new(ministry, cabinet, minister)
[
t("flash.actions.create.notice", resource_name: "Ministry membership"),
"Ministry " + ministry.name,
"Cabinet " + cabinet.name,
"Minister " + minister.name,
"Start Date " + (cabinet.appointment_date + 2.days).strftime("%Y-%m-%d"),
"Created At " + ministry_membership.created_at.strftime("%B %0e, %Y %H:%M"),
"Updated At " + ministry_membership.updated_at.strftime("%B %0e, %Y %H:%M"),
].each do |form_input|
expect(form).to have_content form_input
end
end
This
[
mail_blank_or_taken_error,
blank_password_error,
]
means "call methods mail_blank_or_taken_error(), then blank_password_error() and put their results in an array". You don't have such methods, and that's what the error message tells you. If you wanted those to be strings, make them strings.
[
"mail_blank_or_taken_error",
"blank_password_error",
]
or
%w[
mail_blank_or_taken_error
blank_password_error
]
I found solution to this problem.
The method needed proper argument parsed and page_object reference
def all_mandatory_fields_cant_be_blank_error_displayed_in(form)
[
form.blank_password_error,
form.signs_limit_error,
form.blank_name_error,
form.blank_adress_line_error,
form.blank_city_error,
form.blank_country_error,
form.blank_zipcode_error,
form.blank_phone_number_error,
form.blank_website_error,
].each do |validation_error|
expect(validation_error).to be_present
end
end
When a run a test with JSON, the rspec doesn't show the full spec, so I can't see the diference between return and expected.
The message of diff is shortened with ...
expected: "{\"id\":1,\"number\":1,\"sequential\":1,\"emitted_at\":\"2014-01-01T13:35:21.000Z\",\"status\":\"aut...erenceds_attributes\":[{\"id\":null,\"nfe_key\":\"42150707697707000148550010000020101000020105\"}]}"
got: "{\"id\":1,\"number\":1,\"sequential\":1,\"emitted_at\":\"2014-01-01T13:35:21.000Z\",\"status\":\"aut...erenceds_attributes\":[{\"id\":null,\"nfe_key\":\"42150707697707000148550010000020101000020105\"}]}"
aut...erenceds_attributes look in middle of message
My script test:
RSpec.describe InvoiceSerializer do
let(:invoice) do
build :invoice, :testing_serializer
end
subject { described_class.new invoice }
it "returns a json" do
expected = {
id: 1,
number: 1,
sequential: 1,
emitted_at: "2014-01-01T13:35:21.000Z",
status: "authorized",
invoice_bills_attributes: [{
id: nil,
expire_at: "2014-01-02T00:00:00.000Z",
value: "1.23"
}],
...
}.to_json
expect(subject.to_json).to eq expected
end
end
Example of error in my console
What gem/plugin or expectation that you use to check your test?
I use the console and Rubymine IDE.
Now I use:
puts "1 --> #{subject.to_json}"
puts "2 --> #{expected}"
And I don't like to write this for to debbug my test.
Set RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length to a high value
Update: as Yurri suggested, it might be better to better to set it to nil
This might help: https://github.com/waterlink/rspec-json_expectations
As a bonus, it allows you to specify your tests in terms of a subset of attributes, which can be used to create more granular tests.
To build on previous answers, and utilize the RSpec.configure syntax you'll want to use something like this:
RSpec.configure do |rspec|
rspec.expect_with :rspec do |c|
# Or a very large value, if you do want to truncate at some point
c.max_formatted_output_length = nil
end
end
Capybara 2.1.0 doesn't seem to find any meta tags:
(rdb:1) p page.find 'meta'
*** Capybara::ElementNotFound Exception: Unable to find css "meta"
even when they appear in page.source:
(rdb:1) p page.source
"<!doctype html>\n<html>\n<head>\n<title>MyTitle</title>\n<meta charset='utf-8'>\n<meta content='IE=edge,chrome=1' http-equiv='X-UA-Compatible'>\n<meta content='width=device-width, initial-scale=1' name='viewport'>\n<meta name='description'>\n\n..."
The solution was to use :visible => false either in find or in have_selector:
page.should have_css 'meta[name="description"]', visible: false
or:
page.find 'meta[name="description"]', visible: false
If you want to check more specific meta, including the description text, etc:
https://gist.github.com/Lordnibbler/6247531
RSpec::Matchers.define :have_meta do |name, expected|
match do |actual|
has_css?("meta[name='#{name}'][content='#{expected}']")
end
failure_message_for_should do |actual|
actual = first("meta[name='#{name}']")
if actual
"expected that meta #{name} would have content='#{expected}' but was '#{actual[:content]}'"
else
"expected that meta #{name} would exist with content='#{expected}'"
end
end
end
RSpec::Matchers.define :have_title do |expected|
match do |actual|
has_css?("title", :text => expected)
end
failure_message_for_should do |actual|
actual = first("title")
if actual
"expected that title would have been '#{expected}' but was '#{actual.text}'"
else
"expected that title would exist with '#{expected}'"
end
end
end
Then, look up meta like so:
page.should have_meta(:description, 'Brand new Ruby on Rails TShirts')
Borrowed with love from Spree: https://github.com/spree/spree/blob/master/core/lib/spree/testing_support/capybara_ext.rb
The proposed solution did not work in 2021, but the following did
page.find('//head/meta[name="description"]', visible: false)[:content]
According to https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Element
Element also has access to HTML attributes and other properties of the
element:
bar.value
bar.text
bar[:title]
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.