Add ancestor/sibling assertions and expectations

This commit is contained in:
Thomas Walpole 2019-05-28 12:20:38 -07:00
parent a9405612c3
commit 43ca01252f
15 changed files with 338 additions and 30 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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