1
0
Fork 0
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:
Thomas Walpole 2017-07-10 14:42:15 -07:00
parent 154ce24d55
commit 548c8c00a6
8 changed files with 296 additions and 17 deletions

View file

@ -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

View file

@ -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'

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -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>