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
|
### Added
|
||||||
|
|
||||||
|
* `sibling` and `ancestor` finders added [Thomas Walpole]
|
||||||
* Added built-in driver registrations `:selenium_chrome` and `:selenium_chrome_headless` [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]
|
* 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
|
* 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/title_query'
|
||||||
require 'capybara/queries/current_path_query'
|
require 'capybara/queries/current_path_query'
|
||||||
require 'capybara/queries/match_query'
|
require 'capybara/queries/match_query'
|
||||||
|
require 'capybara/queries/ancestor_query'
|
||||||
|
require 'capybara/queries/sibling_query'
|
||||||
require 'capybara/query'
|
require 'capybara/query'
|
||||||
|
|
||||||
require 'capybara/node/finders'
|
require 'capybara/node/finders'
|
||||||
|
|
|
@ -34,22 +34,66 @@ module Capybara
|
||||||
else
|
else
|
||||||
args.push(session_options: session_options)
|
args.push(session_options: session_options)
|
||||||
end
|
end
|
||||||
query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
|
synced_resolve Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
|
||||||
synchronize(query.wait) do
|
end
|
||||||
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?
|
#
|
||||||
|
# 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
|
else
|
||||||
result = query.resolve_for(self)
|
args.push(session_options: session_options)
|
||||||
end
|
end
|
||||||
if query.match == :one or query.match == :smart and result.size > 1
|
synced_resolve Capybara::Queries::AncestorQuery.new(*args, &optional_filter_block)
|
||||||
raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
|
|
||||||
end
|
end
|
||||||
if result.empty?
|
|
||||||
raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
|
##
|
||||||
|
#
|
||||||
|
# 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
|
end
|
||||||
result.first
|
synced_resolve Capybara::Queries::SiblingQuery.new(*args, &optional_filter_block)
|
||||||
end.tap(&:allow_reload!)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -252,6 +296,25 @@ module Capybara
|
||||||
nil
|
nil
|
||||||
end
|
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
|
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,
|
et dolore magna aliqua. Ut enim ad minim veniam,
|
||||||
quis nostrud exercitation <a href="/foo" id="foo">ullamco</a> laboris nisi
|
quis nostrud exercitation <a href="/foo" id="foo">ullamco</a> laboris nisi
|
||||||
ut aliquip ex ea commodo consequat.
|
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>
|
||||||
|
|
||||||
<p class="para" id="second">
|
<p class="para" id="second">
|
||||||
|
@ -121,3 +121,29 @@ banana</textarea>
|
||||||
<a id="link_placeholder">No href</a>
|
<a id="link_placeholder">No href</a>
|
||||||
<a id="link_blank_href" href="">Blank href</a>
|
<a id="link_blank_href" href="">Blank href</a>
|
||||||
</div>
|
</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…
Add table
Add a link
Reference in a new issue