mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Add ancestor
and sibling
methods to node finders
This commit is contained in:
parent
154ce24d55
commit
548c8c00a6
8 changed files with 296 additions and 17 deletions
|
@ -4,6 +4,7 @@ Release data: unreleased
|
|||
|
||||
### Added
|
||||
|
||||
* `sibling` and `ancestor` finders added [Thomas Walpole]
|
||||
* Added built-in driver registrations `:selenium_chrome` and `:selenium_chrome_headless` [Thomas Walpole]
|
||||
* Add `and_then` to Capybara RSpec matchers which behaves like the previous `and` compounder. [Thomas Walpole]
|
||||
* Compound RSpec expectations with Capybara matchers now run both matchers inside a retry loop rather
|
||||
|
|
|
@ -413,6 +413,8 @@ module Capybara
|
|||
require 'capybara/queries/title_query'
|
||||
require 'capybara/queries/current_path_query'
|
||||
require 'capybara/queries/match_query'
|
||||
require 'capybara/queries/ancestor_query'
|
||||
require 'capybara/queries/sibling_query'
|
||||
require 'capybara/query'
|
||||
|
||||
require 'capybara/node/finders'
|
||||
|
|
|
@ -34,22 +34,66 @@ module Capybara
|
|||
else
|
||||
args.push(session_options: session_options)
|
||||
end
|
||||
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
|
||||
synchronize(query.wait) do
|
||||
if (query.match == :smart or query.match == :prefer_exact)
|
||||
result = query.resolve_for(self, true)
|
||||
result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact?
|
||||
else
|
||||
result = query.resolve_for(self)
|
||||
end
|
||||
if query.match == :one or query.match == :smart and result.size > 1
|
||||
raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
|
||||
end
|
||||
if result.empty?
|
||||
raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
|
||||
end
|
||||
result.first
|
||||
end.tap(&:allow_reload!)
|
||||
synced_resolve Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Find an {Capybara::Node::Element} based on the given arguments that is also an ancestor of the element called on. +ancestor+ will raise an error if the element
|
||||
# is not found.
|
||||
#
|
||||
# +ancestor+ takes the same options as +find+.
|
||||
#
|
||||
# element.ancestor('#foo').find('.bar')
|
||||
# element.ancestor(:xpath, './/div[contains(., "bar")]')
|
||||
# element.ancestor('ul', text: 'Quox').click_link('Delete')
|
||||
#
|
||||
# @param (see Capybara::Node::Finders#find)
|
||||
#
|
||||
# @!macro waiting_behavior
|
||||
#
|
||||
# @option options [Boolean] match The matching strategy to use.
|
||||
#
|
||||
# @return [Capybara::Node::Element] The found element
|
||||
# @raise [Capybara::ElementNotFound] If the element can't be found before time expires
|
||||
#
|
||||
def ancestor(*args, &optional_filter_block)
|
||||
if args.last.is_a? Hash
|
||||
args.last[:session_options] = session_options
|
||||
else
|
||||
args.push(session_options: session_options)
|
||||
end
|
||||
synced_resolve Capybara::Queries::AncestorQuery.new(*args, &optional_filter_block)
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Find an {Capybara::Node::Element} based on the given arguments that is also a sibling of the element called on. +sibling+ will raise an error if the element
|
||||
# is not found.
|
||||
#
|
||||
#
|
||||
# +sibling+ takes the same options as +find+.
|
||||
#
|
||||
# element.sibling('#foo').find('.bar')
|
||||
# element.sibling(:xpath, './/div[contains(., "bar")]')
|
||||
# element.sibling('ul', text: 'Quox').click_link('Delete')
|
||||
#
|
||||
# @param (see Capybara::Node::Finders#find)
|
||||
#
|
||||
# @macro waiting_behavior
|
||||
#
|
||||
# @option options [Boolean] match The matching strategy to use.
|
||||
#
|
||||
# @return [Capybara::Node::Element] The found element
|
||||
# @raise [Capybara::ElementNotFound] If the element can't be found before time expires
|
||||
#
|
||||
def sibling(*args, &optional_filter_block)
|
||||
if args.last.is_a? Hash
|
||||
args.last[:session_options] = session_options
|
||||
else
|
||||
args.push(session_options: session_options)
|
||||
end
|
||||
synced_resolve Capybara::Queries::SiblingQuery.new(*args, &optional_filter_block)
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -252,6 +296,25 @@ module Capybara
|
|||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def synced_resolve(query)
|
||||
synchronize(query.wait) do
|
||||
if (query.match == :smart or query.match == :prefer_exact)
|
||||
result = query.resolve_for(self, true)
|
||||
result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact?
|
||||
else
|
||||
result = query.resolve_for(self)
|
||||
end
|
||||
if query.match == :one or query.match == :smart and result.size > 1
|
||||
raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
|
||||
end
|
||||
if result.empty?
|
||||
raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
|
||||
end
|
||||
result.first
|
||||
end.tap(&:allow_reload!)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
25
lib/capybara/queries/ancestor_query.rb
Normal file
25
lib/capybara/queries/ancestor_query.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
module Capybara
|
||||
module Queries
|
||||
class AncestorQuery < MatchQuery
|
||||
# @api private
|
||||
def resolve_for(node, exact = nil)
|
||||
@resolved_node = node
|
||||
node.synchronize do
|
||||
match_results = super(node.session.current_scope, exact)
|
||||
node.all(:xpath, XPath.ancestor) do |el|
|
||||
match_results.include?(el)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def description
|
||||
desc = super
|
||||
if @resolved_node && (child_query = @resolved_node.instance_variable_get(:@query))
|
||||
desc += " that is an ancestor of #{child_query.description}"
|
||||
end
|
||||
desc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
lib/capybara/queries/sibling_query.rb
Normal file
25
lib/capybara/queries/sibling_query.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
module Capybara
|
||||
module Queries
|
||||
class SiblingQuery < MatchQuery
|
||||
# @api private
|
||||
def resolve_for(node, exact = nil)
|
||||
@resolved_node = node
|
||||
node.synchronize do
|
||||
match_results = super(node.session.current_scope, exact)
|
||||
node.all(:xpath, XPath.preceding_sibling.union(XPath.following_sibling)) do |el|
|
||||
match_results.include?(el)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def description
|
||||
desc = super
|
||||
if @resolved_node && (child_query = @resolved_node.instance_variable_get(:@query))
|
||||
desc += " that is a sibling of #{child_query.description}"
|
||||
end
|
||||
desc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
85
lib/capybara/spec/session/ancestor_spec.rb
Normal file
85
lib/capybara/spec/session/ancestor_spec.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
Capybara::SpecHelper.spec '#ancestor' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
after do
|
||||
Capybara::Selector.remove(:monkey)
|
||||
end
|
||||
|
||||
it "should find the ancestor element using the given locator" do
|
||||
el = @session.find(:css, '#first_image')
|
||||
expect(el.ancestor('//p')).to have_text('Lorem ipsum dolor')
|
||||
expect(el.ancestor("//a")[:'aria-label']).to eq('Go to simple')
|
||||
end
|
||||
|
||||
it "should find the ancestor element using the given locator and options" do
|
||||
el = @session.find(:css, '#child')
|
||||
expect(el.ancestor('//div', text: 'Ancestor Ancestor Ancestor')[:id]).to eq('ancestor3')
|
||||
end
|
||||
|
||||
it "should raise an error if there are multiple matches" do
|
||||
el = @session.find(:css, '#child')
|
||||
expect { el.ancestor('//div') }.to raise_error(Capybara::Ambiguous)
|
||||
expect { el.ancestor('//div', text: 'Ancestor') }.to raise_error(Capybara::Ambiguous)
|
||||
end
|
||||
|
||||
context "with css selectors" do
|
||||
it "should find the first element using the given locator" do
|
||||
el = @session.find(:css, '#first_image')
|
||||
expect(el.ancestor(:css, 'p')).to have_text('Lorem ipsum dolor')
|
||||
expect(el.ancestor(:css, 'a')[:'aria-label']).to eq('Go to simple')
|
||||
end
|
||||
|
||||
it "should support pseudo selectors" do
|
||||
el = @session.find(:css, '#button_img')
|
||||
expect(el.ancestor(:css, 'button:disabled')[:id]).to eq('ancestor_button')
|
||||
end
|
||||
end
|
||||
|
||||
context "with xpath selectors" do
|
||||
it "should find the first element using the given locator" do
|
||||
el = @session.find(:css, '#first_image')
|
||||
expect(el.ancestor(:xpath, '//p')).to have_text('Lorem ipsum dolor')
|
||||
expect(el.ancestor(:xpath, "//a")[:'aria-label']).to eq('Go to simple')
|
||||
end
|
||||
end
|
||||
|
||||
context "with custom selector" do
|
||||
it "should use the custom selector" do
|
||||
Capybara.add_selector(:level) do
|
||||
xpath { |num| ".//*[@id='ancestor#{num}']" }
|
||||
end
|
||||
el = @session.find(:css, '#child')
|
||||
expect(el.ancestor(:level, 1).text).to eq('Ancestor Child')
|
||||
expect(el.ancestor(:level, 3).text).to eq('Ancestor Ancestor Ancestor Child')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
it "should raise ElementNotFound with a useful default message if nothing was found" do
|
||||
el = @session.find(:css, '#child')
|
||||
expect do
|
||||
el.ancestor(:xpath, '//div[@id="nosuchthing"]')
|
||||
end.to raise_error(Capybara::ElementNotFound, "Unable to find xpath \"//div[@id=\\\"nosuchthing\\\"]\" that is an ancestor of visible css \"#child\"")
|
||||
end
|
||||
|
||||
|
||||
|
||||
context "within a scope" do
|
||||
it "should limit the ancestors to inside the scope" do
|
||||
@session.within(:css, '#ancestor2') do
|
||||
el = @session.find(:css, '#child')
|
||||
expect(el.ancestor(:css,'div', text: 'Ancestor')[:id]).to eq('ancestor1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should raise if selector type is unknown" do
|
||||
el = @session.find(:css, '#child')
|
||||
expect do
|
||||
el.ancestor(:unknown, '//h1')
|
||||
end.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
52
lib/capybara/spec/session/sibling_spec.rb
Normal file
52
lib/capybara/spec/session/sibling_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
Capybara::SpecHelper.spec '#sibling' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
after do
|
||||
Capybara::Selector.remove(:monkey)
|
||||
end
|
||||
|
||||
it "should find a prior sibling element using the given locator" do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el.sibling('//div[@data-pre]')[:id]).to eq('pre_sibling')
|
||||
end
|
||||
|
||||
it "should find a following sibling element using the given locator" do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el.sibling('//div[@data-post]')[:id]).to eq('post_sibling')
|
||||
end
|
||||
|
||||
it "should raise an error if there are multiple matches" do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect { el.sibling('//div') }.to raise_error(Capybara::Ambiguous)
|
||||
end
|
||||
|
||||
context "with css selectors" do
|
||||
it "should find the first element using the given locator" do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el.sibling(:css, '#pre_sibling')).to have_text('Pre Sibling')
|
||||
expect(el.sibling(:css, '#post_sibling')).to have_text('Post Sibling')
|
||||
end
|
||||
end
|
||||
|
||||
context "with custom selector" do
|
||||
it "should use the custom selector" do
|
||||
Capybara.add_selector(:data_attribute) do
|
||||
xpath { |attr| ".//*[@data-#{attr}]" }
|
||||
end
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el.sibling(:data_attribute, 'pre').text).to eq('Pre Sibling')
|
||||
expect(el.sibling(:data_attribute, 'post').text).to eq('Post Sibling')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
it "should raise ElementNotFound with a useful default message if nothing was found" do
|
||||
el = @session.find(:css, '#child')
|
||||
expect do
|
||||
el.sibling(:xpath, '//div[@id="nosuchthing"]')
|
||||
end.to raise_error(Capybara::ElementNotFound, "Unable to find xpath \"//div[@id=\\\"nosuchthing\\\"]\" that is a sibling of visible css \"#child\"")
|
||||
end
|
||||
end
|
|
@ -19,7 +19,7 @@
|
|||
et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation <a href="/foo" id="foo">ullamco</a> laboris nisi
|
||||
ut aliquip ex ea commodo consequat.
|
||||
<a href="/with_simple_html" aria-label="Go to simple"><img width="20" height="20" alt="awesome image" /></a>
|
||||
<a href="/with_simple_html" aria-label="Go to simple"><img id="first_image" width="20" height="20" alt="awesome image" /></a>
|
||||
</p>
|
||||
|
||||
<p class="para" id="second">
|
||||
|
@ -121,3 +121,29 @@ banana</textarea>
|
|||
<a id="link_placeholder">No href</a>
|
||||
<a id="link_blank_href" href="">Blank href</a>
|
||||
</div>
|
||||
|
||||
<div id="ancestor3">
|
||||
Ancestor
|
||||
<div id="ancestor2">
|
||||
Ancestor
|
||||
<div id="ancestor1">
|
||||
Ancestor
|
||||
<div id="child">Child</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="ancestor_button" type="submit" disabled>
|
||||
<img id="button_img" width="20" height="20" alt="button img"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="sibling_test">
|
||||
<div id="sibling_wrapper" data-pre=true>
|
||||
<div id="pre_sibling" data-pre=true>Pre Sibling</div>
|
||||
<div id="mid_sibling">Mid Sibling</div>
|
||||
<div id="post_sibling" data-post=true>Post Sibling</div>
|
||||
</div>
|
||||
<div id="other_sibling_wrapper" data-post=true>
|
||||
<div data-pre=true>Pre Sibling</div>
|
||||
<div data-post=true>Post Sibling</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue