Evaluate filters in the context of the selector they are being used in and allow node filters to set detailed error messages
This commit is contained in:
parent
0c4857e3e1
commit
a280697da6
|
@ -60,14 +60,14 @@ module Capybara
|
|||
description(true)
|
||||
end
|
||||
|
||||
def matches_filters?(node)
|
||||
def matches_filters?(node, node_filter_errors = [])
|
||||
return true if (@resolved_node&.== node) && options[:allow_self]
|
||||
|
||||
applied_filters << :system
|
||||
return false unless matches_system_filters?(node)
|
||||
|
||||
applied_filters << :node
|
||||
matches_node_filters?(node) && matches_filter_block?(node)
|
||||
matches_node_filters?(node, node_filter_errors) && matches_filter_block?(node)
|
||||
rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
|
||||
false
|
||||
end
|
||||
|
@ -163,21 +163,23 @@ module Capybara
|
|||
VALID_KEYS + custom_keys
|
||||
end
|
||||
|
||||
def matches_node_filters?(node)
|
||||
def matches_node_filters?(node, errors)
|
||||
unapplied_options = options.keys - valid_keys
|
||||
node_filters.all? do |filter_name, filter|
|
||||
if filter.matcher?
|
||||
unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
|
||||
unapplied_options.delete(option_name)
|
||||
filter.matches?(node, option_name, options[option_name])
|
||||
@selector.with_filter_errors(errors) do
|
||||
node_filters.all? do |filter_name, filter|
|
||||
if filter.matcher?
|
||||
unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
|
||||
unapplied_options.delete(option_name)
|
||||
filter.matches?(node, option_name, options[option_name], @selector)
|
||||
end
|
||||
elsif options.key?(filter_name)
|
||||
unapplied_options.delete(filter_name)
|
||||
filter.matches?(node, filter_name, options[filter_name], @selector)
|
||||
elsif filter.default?
|
||||
filter.matches?(node, filter_name, filter.default, @selector)
|
||||
else
|
||||
true
|
||||
end
|
||||
elsif options.key?(filter_name)
|
||||
unapplied_options.delete(filter_name)
|
||||
filter.matches?(node, filter_name, options[filter_name])
|
||||
elsif filter.default?
|
||||
filter.matches?(node, filter_name, filter.default)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -268,13 +270,13 @@ module Capybara
|
|||
if ef.matcher?
|
||||
unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
|
||||
unapplied_options.delete(option_name)
|
||||
ef.apply_filter(memo, option_name, options[option_name])
|
||||
ef.apply_filter(memo, option_name, options[option_name], @selector)
|
||||
end
|
||||
elsif options.key?(name)
|
||||
unapplied_options.delete(name)
|
||||
ef.apply_filter(expr, name, options[name])
|
||||
ef.apply_filter(expr, name, options[name], @selector)
|
||||
elsif ef.default?
|
||||
ef.apply_filter(expr, name, ef.default)
|
||||
ef.apply_filter(expr, name, ef.default, @selector)
|
||||
else
|
||||
expr
|
||||
end
|
||||
|
|
|
@ -28,7 +28,8 @@ module Capybara
|
|||
def initialize(elements, query)
|
||||
@elements = elements
|
||||
@result_cache = []
|
||||
@results_enum = lazy_select_elements { |node| query.matches_filters?(node) }
|
||||
@filter_errors = []
|
||||
@results_enum = lazy_select_elements { |node| query.matches_filters?(node, @filter_errors) }
|
||||
@query = query
|
||||
end
|
||||
|
||||
|
@ -113,7 +114,8 @@ module Capybara
|
|||
end
|
||||
unless rest.empty?
|
||||
elements = rest.map { |el| el.text rescue '<<ERROR>>' }.map(&:inspect).join(', ') # rubocop:disable Style/RescueModifier
|
||||
message << '. Also found ' << elements << ', which matched the selector but not all filters.'
|
||||
message << '. Also found ' << elements << ', which matched the selector but not all filters. '
|
||||
message << @filter_errors.join('. ') if (rest.size == 1) && count.zero?
|
||||
end
|
||||
message
|
||||
end
|
||||
|
|
|
@ -59,7 +59,10 @@ Capybara.add_selector(:field) do
|
|||
|
||||
node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
|
||||
node_filter(:with) do |node, with|
|
||||
with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
|
||||
val = node.value
|
||||
(with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
|
||||
add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters do |type: nil, **options|
|
||||
|
@ -109,7 +112,9 @@ Capybara.add_selector(:link) do
|
|||
|
||||
node_filter(:href) do |node, href|
|
||||
# If not a Regexp it's been handled in the main XPath
|
||||
href.is_a?(Regexp) ? node[:href].match(href) : true
|
||||
(href.is_a?(Regexp) ? node[:href].match(href) : true).tap do |res|
|
||||
add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
|
||||
end
|
||||
end
|
||||
|
||||
expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
|
||||
|
@ -178,7 +183,9 @@ end
|
|||
Capybara.add_selector(:link_or_button) do
|
||||
label 'link or button'
|
||||
xpath do |locator, **options|
|
||||
self.class.all.values_at(:link, :button).map { |selector| selector.call(locator, **options, selector_config: @config) }.reduce(:union)
|
||||
self.class.all.values_at(:link, :button).map do |selector|
|
||||
instance_exec(locator, options, &selector.xpath)
|
||||
end.reduce(:union)
|
||||
end
|
||||
|
||||
node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == 'a' || !(value ^ node.disabled?) }
|
||||
|
@ -210,7 +217,10 @@ Capybara.add_selector(:fillable_field) do
|
|||
filter_set(:_field, %i[disabled multiple name placeholder])
|
||||
|
||||
node_filter(:with) do |node, with|
|
||||
with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
|
||||
val = node.value
|
||||
(with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
|
||||
add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters
|
||||
|
@ -231,7 +241,12 @@ Capybara.add_selector(:radio_button) do
|
|||
|
||||
filter_set(:_field, %i[checked unchecked disabled name])
|
||||
|
||||
node_filter(:option) { |node, value| node.value == value.to_s }
|
||||
node_filter(:option) do |node, value|
|
||||
val = node.value
|
||||
(val == value.to_s).tap do |res|
|
||||
add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters
|
||||
describe_node_filters do |option: nil, **|
|
||||
|
@ -249,7 +264,12 @@ Capybara.add_selector(:checkbox) do
|
|||
|
||||
filter_set(:_field, %i[checked unchecked disabled name])
|
||||
|
||||
node_filter(:option) { |node, value| node.value == value.to_s }
|
||||
node_filter(:option) do |node, value|
|
||||
val = node.value
|
||||
(val == value.to_s).tap do |res|
|
||||
add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters
|
||||
describe_node_filters do |option: nil, **|
|
||||
|
@ -273,7 +293,9 @@ Capybara.add_selector(:select) do
|
|||
else
|
||||
node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
|
||||
end
|
||||
options.sort == actual.sort
|
||||
(options.sort == actual.sort).tap do |res|
|
||||
add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
expression_filter(:with_options) do |expr, options|
|
||||
|
@ -284,12 +306,16 @@ Capybara.add_selector(:select) do
|
|||
|
||||
node_filter(:selected) do |node, selected|
|
||||
actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
|
||||
Array(selected).sort == actual.sort
|
||||
(Array(selected).sort == actual.sort).tap do |res|
|
||||
add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
node_filter(:with_selected) do |node, selected|
|
||||
actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
|
||||
(Array(selected) - actual).empty?
|
||||
(Array(selected) - actual).empty?.tap do |res|
|
||||
add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters do |with_options: nil, **opts|
|
||||
|
@ -320,7 +346,9 @@ Capybara.add_selector(:datalist_input) do
|
|||
|
||||
node_filter(:options) do |node, options|
|
||||
actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
|
||||
options.sort == actual.sort
|
||||
(options.sort == actual.sort).tap do |res|
|
||||
add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
expression_filter(:with_options) do |expr, options|
|
||||
|
@ -471,7 +499,11 @@ Capybara.add_selector(:element) do
|
|||
end
|
||||
|
||||
node_filter(:attributes, matcher: /.+/) do |node, name, val|
|
||||
val.is_a?(Regexp) ? node[name] =~ val : true
|
||||
next true unless val.is_a?(Regexp)
|
||||
|
||||
(node[name] =~ val).tap do |res|
|
||||
add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
|
||||
end
|
||||
end
|
||||
|
||||
describe_expression_filters do |**options|
|
||||
|
|
|
@ -7,18 +7,9 @@ module Capybara
|
|||
def initialize(name, matcher, block, **options)
|
||||
@name = name
|
||||
@matcher = matcher
|
||||
@block = block
|
||||
@options = options
|
||||
@options[:valid_values] = [true, false] if options[:boolean]
|
||||
@block = if boolean?
|
||||
proc do |node, value|
|
||||
error_cnt = errors.size
|
||||
block.call(node, value).tap do |res|
|
||||
add_error("Expected #{@name} #{value} but it wasn't") if !res && error_cnt == errors.size
|
||||
end
|
||||
end
|
||||
else
|
||||
block
|
||||
end
|
||||
end
|
||||
|
||||
def default?
|
||||
|
|
|
@ -6,7 +6,21 @@ module Capybara
|
|||
class Selector
|
||||
module Filters
|
||||
class NodeFilter < Base
|
||||
def matches?(node, name, value, context=nil)
|
||||
def initialize(name, matcher, block, **options)
|
||||
super
|
||||
@block = if boolean?
|
||||
proc do |node, value|
|
||||
error_cnt = errors.size
|
||||
block.call(node, value).tap do |res|
|
||||
add_error("Expected #{name} #{value} but it wasn't") if !res && error_cnt == errors.size
|
||||
end
|
||||
end
|
||||
else
|
||||
block
|
||||
end
|
||||
end
|
||||
|
||||
def matches?(node, name, value, context = nil)
|
||||
apply(node, name, value, true, context)
|
||||
rescue Capybara::ElementNotFound
|
||||
false
|
||||
|
|
|
@ -389,6 +389,10 @@ module Capybara
|
|||
vis.nil? ? fallback : vis
|
||||
end
|
||||
|
||||
def add_error(error_msg)
|
||||
errors << error_msg
|
||||
end
|
||||
|
||||
# @api private
|
||||
def builder
|
||||
case format
|
||||
|
@ -401,8 +405,20 @@ module Capybara
|
|||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def with_filter_errors(errors)
|
||||
Thread.current["capybara_#{object_id}_errors"] = errors
|
||||
yield
|
||||
ensure
|
||||
Thread.current["capybara_#{object_id}_errors"] = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def errors
|
||||
Thread.current["capybara_#{object_id}_errors"] || []
|
||||
end
|
||||
|
||||
def enable_aria_label
|
||||
@config[:enable_aria_label]
|
||||
end
|
||||
|
|
|
@ -41,6 +41,23 @@ Capybara::SpecHelper.spec '#has_field' do
|
|||
expect(@session).not_to have_field('First Name', with: 'John')
|
||||
expect(@session).not_to have_field('First Name', with: /John|Paul|George|Ringo/)
|
||||
end
|
||||
|
||||
it 'should output filter errors if only one element matched the selector but failed the filters' do
|
||||
@session.fill_in('First Name', with: 'Thomas')
|
||||
expect do
|
||||
expect(@session).to have_field('First Name', with: 'Jonas')
|
||||
end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected value to be "Jonas" but was "Thomas"/)
|
||||
|
||||
# native boolean node filter
|
||||
expect do
|
||||
expect(@session).to have_field('First Name', readonly: true)
|
||||
end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected readonly true but it wasn't/)
|
||||
|
||||
# inherited boolean node filter
|
||||
expect do
|
||||
expect(@session).to have_field('form_pets_cat', checked: true)
|
||||
end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected checked true but it wasn't/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with type' do
|
||||
|
|
Loading…
Reference in New Issue