Skip to content

Commit

Permalink
Add ancestor/sibling assertions and expectations
Browse files Browse the repository at this point in the history
  • Loading branch information
twalpole committed May 29, 2019
1 parent a940561 commit 43ca012
Show file tree
Hide file tree
Showing 15 changed files with 338 additions and 30 deletions.
15 changes: 14 additions & 1 deletion lib/capybara/minitest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,20 @@ def assert_#{assertion_name} *args
# @!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
Expand All @@ -102,6 +113,8 @@ def assert_#{assertion_name} *args, &optional_filter_block

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|
Expand Down
27 changes: 20 additions & 7 deletions lib/capybara/minitest/spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ module Expectations
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}]]
Expand Down Expand Up @@ -178,6 +179,18 @@ def must_have_style(*args, &block)
#
# @!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
Expand Down
86 changes: 84 additions & 2 deletions lib/capybara/node/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,88 @@ def has_no_text?(*args, **options)
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
Expand All @@ -743,9 +825,9 @@ def _verify_multiple(*args, wait: nil, **options)
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
Expand Down
16 changes: 9 additions & 7 deletions lib/capybara/queries/ancestor_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -18,12 +26,6 @@ def description(applied = false)
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
15 changes: 11 additions & 4 deletions lib/capybara/queries/sibling_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions lib/capybara/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def empty?
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
Expand Down
17 changes: 16 additions & 1 deletion lib/capybara/rspec/matchers.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -138,7 +140,8 @@ def have_style(styles, **options)
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
Expand All @@ -151,6 +154,18 @@ def have_style(styles, **options)
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
Expand Down
30 changes: 30 additions & 0 deletions lib/capybara/rspec/matchers/have_ancestor.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions lib/capybara/rspec/matchers/have_sibling.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 44 additions & 0 deletions lib/capybara/spec/session/has_ancestor_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 43ca012

Please sign in to comment.