mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Add ancestor/sibling assertions and expectations
This commit is contained in:
parent
a9405612c3
commit
43ca01252f
15 changed files with 338 additions and 30 deletions
|
@ -86,9 +86,20 @@ module Capybara
|
|||
# @!method assert_matches_style
|
||||
# see {Capybara::Node::Matchers#assert_matches_style}
|
||||
|
||||
## Assert element has a matching sibling
|
||||
#
|
||||
# @!method assert_sibling
|
||||
# see {Capybara::Node::Matchers#assert_sibling}
|
||||
|
||||
## Assert element has a matching ancestor
|
||||
#
|
||||
# @!method assert_ancestor
|
||||
# see {Capybara::Node::Matchers#assert_ancestor}
|
||||
|
||||
%w[selector no_selector matches_style
|
||||
all_of_selectors none_of_selectors any_of_selectors
|
||||
matches_selector not_matches_selector].each do |assertion_name|
|
||||
matches_selector not_matches_selector
|
||||
sibling no_sibling ancestor no_ancestor].each do |assertion_name|
|
||||
class_eval <<-ASSERTION, __FILE__, __LINE__ + 1
|
||||
def assert_#{assertion_name} *args, &optional_filter_block
|
||||
self.assertions +=1
|
||||
|
@ -102,6 +113,8 @@ module Capybara
|
|||
|
||||
alias_method :refute_selector, :assert_no_selector
|
||||
alias_method :refute_matches_selector, :assert_not_matches_selector
|
||||
alias_method :refute_ancestor, :assert_no_ancestor
|
||||
alias_method :refute_sibling, :assert_no_sibling
|
||||
|
||||
%w[xpath css link button field select table].each do |selector_type|
|
||||
define_method "assert_#{selector_type}" do |*args, &optional_filter_block|
|
||||
|
|
|
@ -11,13 +11,14 @@ module Capybara
|
|||
end
|
||||
|
||||
# rubocop:disable Style/MultilineBlockChain
|
||||
(%w[selector xpath css link button field select table checked_field unchecked_field].flat_map do |assertion|
|
||||
[%W[assert_#{assertion} must_have_#{assertion}],
|
||||
%W[refute_#{assertion} wont_have_#{assertion}]]
|
||||
end + [%w[assert_all_of_selectors must_have_all_of_selectors],
|
||||
%w[assert_none_of_selectors must_have_none_of_selectors],
|
||||
%w[assert_any_of_selectors must_have_any_of_selectors],
|
||||
%w[assert_matches_style must_match_style]] +
|
||||
(%w[selector xpath css link button field select table checked_field unchecked_field
|
||||
ancestor sibling].flat_map do |assertion|
|
||||
[%W[assert_#{assertion} must_have_#{assertion}],
|
||||
%W[refute_#{assertion} wont_have_#{assertion}]]
|
||||
end + [%w[assert_all_of_selectors must_have_all_of_selectors],
|
||||
%w[assert_none_of_selectors must_have_none_of_selectors],
|
||||
%w[assert_any_of_selectors must_have_any_of_selectors],
|
||||
%w[assert_matches_style must_match_style]] +
|
||||
%w[selector xpath css].flat_map do |assertion|
|
||||
[%W[assert_matches_#{assertion} must_match_#{assertion}],
|
||||
%W[refute_matches_#{assertion} wont_match_#{assertion}]]
|
||||
|
@ -178,6 +179,18 @@ module Capybara
|
|||
#
|
||||
# @!method must_match_style
|
||||
# see {Capybara::Node::Matchers#assert_matches_style}
|
||||
|
||||
##
|
||||
# Expectation that there is an ancestor
|
||||
#
|
||||
# @!method must_have_ancestor
|
||||
# see Capybara::Node::Matchers#has_ancestor?
|
||||
|
||||
##
|
||||
# Expectation that there is a sibling
|
||||
#
|
||||
# @!method must_have_sibling
|
||||
# see Capybara::Node::Matchers#has_sibling?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -725,6 +725,88 @@ module Capybara
|
|||
end
|
||||
alias_method :has_no_content?, :has_no_text?
|
||||
|
||||
##
|
||||
#
|
||||
# Asserts that a given selector matches an ancestor of the current node.
|
||||
#
|
||||
# element.assert_ancestor('p#foo')
|
||||
#
|
||||
# Accepts the same options as {#assert_selector}
|
||||
#
|
||||
# @param (see Capybara::Node::Finders#find)
|
||||
# @raise [Capybara::ExpectationNotMet] If the selector does not exist
|
||||
#
|
||||
def assert_ancestor(*args, &optional_filter_block)
|
||||
_verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
|
||||
raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count? && (result.any? || query.expects_none?)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_no_ancestor(*args, &optional_filter_block)
|
||||
_verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
|
||||
if result.matches_count? && (!result.empty? || query.expects_none?)
|
||||
raise Capybara::ExpectationNotMet, result.negative_failure_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Predicate version of {#assert_ancestor}
|
||||
#
|
||||
def has_ancestor?(*args, **options, &optional_filter_block)
|
||||
make_predicate(options) { assert_ancestor(*args, options, &optional_filter_block) }
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Predicate version of {#assert_no_ancestor}
|
||||
#
|
||||
def has_no_ancestor?(*args, **options, &optional_filter_block)
|
||||
make_predicate(options) { assert_no_ancestor(*args, options, &optional_filter_block) }
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Asserts that a given selector matches a sibling of the current node.
|
||||
#
|
||||
# element.assert_sibling('p#foo')
|
||||
#
|
||||
# Accepts the same options as {#assert_selector}
|
||||
#
|
||||
# @param (see Capybara::Node::Finders#find)
|
||||
# @raise [Capybara::ExpectationNotMet] If the selector does not exist
|
||||
#
|
||||
def assert_sibling(*args, &optional_filter_block)
|
||||
_verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
|
||||
raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count? && (result.any? || query.expects_none?)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_no_sibling(*args, &optional_filter_block)
|
||||
_verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
|
||||
if result.matches_count? && (!result.empty? || query.expects_none?)
|
||||
raise Capybara::ExpectationNotMet, result.negative_failure_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Predicate version of {#assert_sibling}
|
||||
#
|
||||
def has_sibling?(*args, **options, &optional_filter_block)
|
||||
make_predicate(options) { assert_sibling(*args, options, &optional_filter_block) }
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Predicate version of {#assert_no_sibling}
|
||||
#
|
||||
def has_no_sibling?(*args, **options, &optional_filter_block)
|
||||
make_predicate(options) { assert_no_sibling(*args, options, &optional_filter_block) }
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
eql?(other) || (other.respond_to?(:base) && base == other.base)
|
||||
end
|
||||
|
@ -743,9 +825,9 @@ module Capybara
|
|||
end
|
||||
end
|
||||
|
||||
def _verify_selector_result(query_args, optional_filter_block)
|
||||
def _verify_selector_result(query_args, optional_filter_block, query_type = Capybara::Queries::SelectorQuery)
|
||||
query_args = _set_query_session_options(*query_args)
|
||||
query = Capybara::Queries::SelectorQuery.new(*query_args, &optional_filter_block)
|
||||
query = query_type.new(*query_args, &optional_filter_block)
|
||||
synchronize(query.wait) do
|
||||
yield query.resolve_for(self), query
|
||||
end
|
||||
|
|
|
@ -3,12 +3,20 @@
|
|||
module Capybara
|
||||
module Queries
|
||||
class AncestorQuery < Capybara::Queries::SelectorQuery
|
||||
def initialize(*args)
|
||||
super
|
||||
@count_options = {}
|
||||
COUNT_KEYS.each do |key|
|
||||
@count_options[key] = @options.delete(key) if @options.key?(key)
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def resolve_for(node, exact = nil)
|
||||
@child_node = node
|
||||
node.synchronize do
|
||||
match_results = super(node.session.current_scope, exact)
|
||||
node.all(:xpath, XPath.ancestor) { |el| match_results.include?(el) }
|
||||
node.all(:xpath, XPath.ancestor, **@count_options) { |el| match_results.include?(el) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -18,12 +26,6 @@ module Capybara
|
|||
desc += " that is an ancestor of #{child_query.description}" if child_query
|
||||
desc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_keys
|
||||
super - COUNT_KEYS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,15 +2,22 @@
|
|||
|
||||
module Capybara
|
||||
module Queries
|
||||
class SiblingQuery < MatchQuery
|
||||
class SiblingQuery < SelectorQuery
|
||||
def initialize(*args)
|
||||
super
|
||||
@count_options = {}
|
||||
COUNT_KEYS.each do |key|
|
||||
@count_options[key] = @options.delete(key) if @options.key?(key)
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def resolve_for(node, exact = nil)
|
||||
@sibling_node = node
|
||||
node.synchronize do
|
||||
match_results = super(node.session.current_scope, exact)
|
||||
node.all(:xpath, XPath.preceding_sibling + XPath.following_sibling) do |el|
|
||||
match_results.include?(el)
|
||||
end
|
||||
xpath = XPath.preceding_sibling + XPath.following_sibling
|
||||
node.all(:xpath, xpath, **@count_options) { |el| match_results.include?(el) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -76,6 +76,8 @@ module Capybara
|
|||
end
|
||||
|
||||
def compare_count
|
||||
return 0 unless @query
|
||||
|
||||
count, min, max, between = @query.options.values_at(:count, :minimum, :maximum, :between)
|
||||
|
||||
# Only check filters for as many elements as necessary to determine result
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'capybara/rspec/matchers/have_selector'
|
||||
require 'capybara/rspec/matchers/have_ancestor'
|
||||
require 'capybara/rspec/matchers/have_sibling'
|
||||
require 'capybara/rspec/matchers/match_selector'
|
||||
require 'capybara/rspec/matchers/have_current_path'
|
||||
require 'capybara/rspec/matchers/match_style'
|
||||
|
@ -138,7 +140,8 @@ module Capybara
|
|||
end
|
||||
|
||||
%w[selector css xpath text title current_path link button
|
||||
field checked_field unchecked_field select table].each do |matcher_type|
|
||||
field checked_field unchecked_field select table
|
||||
sibling ancestor].each do |matcher_type|
|
||||
define_method "have_no_#{matcher_type}" do |*args, &optional_filter_block|
|
||||
Matchers::NegatedMatcher.new(send("have_#{matcher_type}", *args, &optional_filter_block))
|
||||
end
|
||||
|
@ -151,6 +154,18 @@ module Capybara
|
|||
end
|
||||
end
|
||||
|
||||
# RSpec matcher for whether sibling element(s) matching a given selector exist
|
||||
# See {Capybara::Node::Matcher#assert_sibling}
|
||||
def have_sibling(*args, &optional_filter_block)
|
||||
Matchers::HaveSibling.new(*args, &optional_filter_block)
|
||||
end
|
||||
|
||||
# RSpec matcher for whether ancestor element(s) matching a given selector exist
|
||||
# See {Capybara::Node::Matcher#assert_ancestor}
|
||||
def have_ancestor(*args, &optional_filter_block)
|
||||
Matchers::HaveAncestor.new(*args, &optional_filter_block)
|
||||
end
|
||||
|
||||
##
|
||||
# Wait for window to become closed.
|
||||
# @example
|
||||
|
|
30
lib/capybara/rspec/matchers/have_ancestor.rb
Normal file
30
lib/capybara/rspec/matchers/have_ancestor.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'capybara/rspec/matchers/base'
|
||||
require 'capybara/rspec/matchers/count_sugar'
|
||||
|
||||
module Capybara
|
||||
module RSpecMatchers
|
||||
module Matchers
|
||||
class HaveAncestor < WrappedElementMatcher
|
||||
include CountSugar
|
||||
|
||||
def element_matches?(el)
|
||||
el.assert_ancestor(*@args, &@filter_block)
|
||||
end
|
||||
|
||||
def element_does_not_match?(el)
|
||||
el.assert_no_ancestor(*@args, &@filter_block)
|
||||
end
|
||||
|
||||
def description
|
||||
"have ancestor #{query.description}"
|
||||
end
|
||||
|
||||
def query
|
||||
@query ||= Capybara::Queries::AncestorQuery.new(*session_query_args, &@filter_block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
30
lib/capybara/rspec/matchers/have_sibling.rb
Normal file
30
lib/capybara/rspec/matchers/have_sibling.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'capybara/rspec/matchers/base'
|
||||
require 'capybara/rspec/matchers/count_sugar'
|
||||
|
||||
module Capybara
|
||||
module RSpecMatchers
|
||||
module Matchers
|
||||
class HaveSibling < WrappedElementMatcher
|
||||
include CountSugar
|
||||
|
||||
def element_matches?(el)
|
||||
el.assert_sibling(*@args, &@filter_block)
|
||||
end
|
||||
|
||||
def element_does_not_match?(el)
|
||||
el.assert_no_sibling(*@args, &@filter_block)
|
||||
end
|
||||
|
||||
def description
|
||||
"have sibling #{query.description}"
|
||||
end
|
||||
|
||||
def query
|
||||
@query ||= Capybara::Queries::SiblingQuery.new(*session_query_args, &@filter_block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
44
lib/capybara/spec/session/has_ancestor_spec.rb
Normal file
44
lib/capybara/spec/session/has_ancestor_spec.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Capybara::SpecHelper.spec '#have_ancestor' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
it 'should assert an ancestor using the given locator' do
|
||||
el = @session.find(:css, '#ancestor1')
|
||||
expect(el).to have_ancestor(:css, '#ancestor2')
|
||||
end
|
||||
|
||||
it 'should assert an ancestor even if not parent' do
|
||||
el = @session.find(:css, '#child')
|
||||
expect(el).to have_ancestor(:css, '#ancestor3')
|
||||
end
|
||||
|
||||
it 'should not raise an error if there are multiple matches' do
|
||||
el = @session.find(:css, '#child')
|
||||
expect(el).to have_ancestor(:css, 'div')
|
||||
end
|
||||
|
||||
it 'should allow counts to be specified' do
|
||||
el = @session.find(:css, '#child')
|
||||
|
||||
expect do
|
||||
expect(el).to have_ancestor(:css, 'div').once
|
||||
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
||||
|
||||
expect(el).to have_ancestor(:css, 'div').exactly(3).times
|
||||
end
|
||||
end
|
||||
|
||||
Capybara::SpecHelper.spec '#have_no_ancestor' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
it 'should assert no matching ancestor' do
|
||||
el = @session.find(:css, '#ancestor1')
|
||||
expect(el).to have_no_ancestor(:css, '#child')
|
||||
expect(el).not_to have_ancestor(:css, '#child')
|
||||
end
|
||||
end
|
50
lib/capybara/spec/session/has_sibling_spec.rb
Normal file
50
lib/capybara/spec/session/has_sibling_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Capybara::SpecHelper.spec '#have_sibling' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
it 'should assert a prior sibling element using the given locator' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el).to have_sibling(:css, '#pre_sibling')
|
||||
end
|
||||
|
||||
it 'should assert a following sibling element using the given locator' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el).to have_sibling(:css, '#post_sibling')
|
||||
end
|
||||
|
||||
it 'should not raise an error if there are multiple matches' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el).to have_sibling(:css, 'div')
|
||||
end
|
||||
|
||||
it 'should allow counts to be specified' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
|
||||
expect(el).to have_sibling(:css, 'div').exactly(2).times
|
||||
expect do
|
||||
expect(el).to have_sibling(:css, 'div').once
|
||||
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
||||
end
|
||||
end
|
||||
|
||||
Capybara::SpecHelper.spec '#have_no_sibling' do
|
||||
before do
|
||||
@session.visit('/with_html')
|
||||
end
|
||||
|
||||
it 'should assert no matching sibling' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect(el).to have_no_sibling(:css, '#not_a_sibling')
|
||||
expect(el).not_to have_sibling(:css, '#not_a_sibling')
|
||||
end
|
||||
|
||||
it 'should raise if there are matching siblings' do
|
||||
el = @session.find(:css, '#mid_sibling')
|
||||
expect do
|
||||
expect(el).to have_no_sibling(:css, '#pre_sibling')
|
||||
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
||||
end
|
||||
end
|
|
@ -42,13 +42,13 @@ Capybara::SpecHelper.spec '#select' do
|
|||
end
|
||||
|
||||
it 'should select an option from a select box by id' do
|
||||
@session.select('Finish', from: 'form_locale')
|
||||
@session.select('Finnish', from: 'form_locale')
|
||||
@session.click_button('awesome')
|
||||
expect(extract_results(@session)['locale']).to eq('fi')
|
||||
end
|
||||
|
||||
it 'should select an option from a select box by label' do
|
||||
@session.select('Finish', from: 'Locale')
|
||||
@session.select('Finnish', from: 'Locale')
|
||||
@session.click_button('awesome')
|
||||
expect(extract_results(@session)['locale']).to eq('fi')
|
||||
end
|
||||
|
@ -183,7 +183,7 @@ Capybara::SpecHelper.spec '#select' do
|
|||
context 'with :exact option' do
|
||||
context 'when `false`' do
|
||||
it 'can match select box approximately' do
|
||||
@session.select('Finish', from: 'Loc', exact: false)
|
||||
@session.select('Finnish', from: 'Loc', exact: false)
|
||||
@session.click_button('awesome')
|
||||
expect(extract_results(@session)['locale']).to eq('fi')
|
||||
end
|
||||
|
@ -204,13 +204,13 @@ Capybara::SpecHelper.spec '#select' do
|
|||
context 'when `true`' do
|
||||
it 'can match select box approximately' do
|
||||
expect do
|
||||
@session.select('Finish', from: 'Loc', exact: true)
|
||||
@session.select('Finnish', from: 'Loc', exact: true)
|
||||
end.to raise_error(Capybara::ElementNotFound)
|
||||
end
|
||||
|
||||
it 'can match option approximately' do
|
||||
expect do
|
||||
@session.select('Fin', from: 'Locale', exact: true)
|
||||
@session.select('Fin', from: 'Locale', exact: true)
|
||||
end.to raise_error(Capybara::ElementNotFound)
|
||||
end
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
<select name="form[locale]" id="form_locale">
|
||||
<option value="sv">Swedish</option>
|
||||
<option selected="selected" value="en">English</option>
|
||||
<option value="fi">Finish</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="no">Norwegian</option>
|
||||
<option value="jo">John's made-up language</option>
|
||||
<option value="jbo"> Lojban </option>
|
||||
|
|
|
@ -130,6 +130,16 @@ class MinitestTest < Minitest::Test
|
|||
visit('/with_html')
|
||||
assert_matches_style(find(:css, '#second'), display: 'inline')
|
||||
end
|
||||
|
||||
def test_assert_ancestor
|
||||
option = find(:option, 'Finnish')
|
||||
assert_ancestor(option, :css, '#form_locale')
|
||||
end
|
||||
|
||||
def test_assert_sibling
|
||||
option = find(:css, '#form_title').find(:option, 'Mrs')
|
||||
assert_sibling(option, :option, 'Mr')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'capybara/minitest' do
|
||||
|
@ -148,6 +158,6 @@ RSpec.describe 'capybara/minitest' do
|
|||
reporter.start
|
||||
MinitestTest.run reporter, {}
|
||||
reporter.report
|
||||
expect(output.string).to include('20 runs, 50 assertions, 0 failures, 0 errors, 1 skips')
|
||||
expect(output.string).to include('22 runs, 52 assertions, 0 failures, 0 errors, 1 skips')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -127,6 +127,16 @@ class MinitestSpecTest < Minitest::Spec
|
|||
find(:css, '#second').must_have_style('display' => 'inline') # deprecated
|
||||
find(:css, '#second').must_match_style('display' => 'inline')
|
||||
end
|
||||
|
||||
it 'supports ancestor expectations' do
|
||||
option = find(:option, 'Finnish')
|
||||
option.must_have_ancestor(:css, '#form_locale')
|
||||
end
|
||||
|
||||
it 'supports sibling expectations' do
|
||||
option = find(:css, '#form_title').find(:option, 'Mrs')
|
||||
option.must_have_sibling(:option, 'Mr')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'capybara/minitest/spec' do
|
||||
|
@ -145,7 +155,7 @@ RSpec.describe 'capybara/minitest/spec' do
|
|||
reporter.start
|
||||
MinitestSpecTest.run reporter, {}
|
||||
reporter.report
|
||||
expect(output.string).to include('20 runs, 42 assertions, 1 failures, 0 errors, 1 skips')
|
||||
expect(output.string).to include('22 runs, 44 assertions, 1 failures, 0 errors, 1 skips')
|
||||
# Make sure error messages are displayed
|
||||
expect(output.string).to match(/expected to find select box "non_existing_form_title" .*but there were no matches/)
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue