Support XPath:Expression for built-in id and class filters when using XPath based selectors
Replace Regexp named filters with a :matcher option for filters
This commit is contained in:
parent
f14a2dd5b4
commit
daa6a4c7da
|
@ -87,8 +87,7 @@ module Capybara
|
|||
end
|
||||
|
||||
def assert_valid_keys
|
||||
regex_keys, string_keys = valid_keys.group_by { |k| k.is_a? Regexp }.fetch_values(true, false) { [] }
|
||||
invalid_keys = (@options.keys - string_keys).reject { |k| regex_keys.any? { |r| r =~ k } }
|
||||
invalid_keys = @options.keys - valid_keys
|
||||
return if invalid_keys.empty?
|
||||
|
||||
invalid_names = invalid_keys.map(&:inspect).join(", ")
|
||||
|
|
|
@ -9,6 +9,14 @@ module Capybara
|
|||
|
||||
private
|
||||
|
||||
def assert_valid_keys
|
||||
invalid_options = @options.keys & COUNT_KEYS
|
||||
unless invalid_options.empty?
|
||||
raise ArgumentError, "Match queries don't support quantity options. Invalid keys - #{invalid_options.join(', ')}"
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def valid_keys
|
||||
super - COUNT_KEYS
|
||||
end
|
||||
|
|
|
@ -129,8 +129,8 @@ module Capybara
|
|||
unapplied_options = options.keys - valid_keys
|
||||
|
||||
node_filters.all? do |filter_name, filter|
|
||||
if filter_name.is_a?(Regexp)
|
||||
unapplied_options.grep(filter_name).all? do |option_name|
|
||||
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])
|
||||
end
|
||||
|
@ -174,27 +174,57 @@ module Capybara
|
|||
end
|
||||
|
||||
def assert_valid_keys
|
||||
super
|
||||
return if VALID_MATCH.include?(match)
|
||||
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
|
||||
unless VALID_MATCH.include?(match)
|
||||
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
|
||||
end
|
||||
unhandled_options = @options.keys - valid_keys
|
||||
unhandled_options -= @options.keys.select do |option_name|
|
||||
expression_filters.any? { |_nmae, ef| ef.handles_option? option_name } ||
|
||||
node_filters.any? { |_name, nf| nf.handles_option? option_name }
|
||||
end
|
||||
|
||||
return if unhandled_options.empty?
|
||||
invalid_names = unhandled_options.map(&:inspect).join(", ")
|
||||
valid_names = valid_keys.map(&:inspect).join(", ")
|
||||
raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
|
||||
end
|
||||
|
||||
def filtered_xpath(expr)
|
||||
expr = "(#{expr})[#{XPath.attr(:id) == options[:id]}]" if options.key?(:id) && !custom_keys.include?(:id)
|
||||
if options.key?(:id) && !custom_keys.include?(:id)
|
||||
expr = if options[:id].is_a? XPath::Expression
|
||||
"(#{expr})[#{XPath.attr(:id)[options[:id]]}]"
|
||||
else
|
||||
"(#{expr})[#{XPath.attr(:id) == options[:id]}]"
|
||||
end
|
||||
end
|
||||
if options.key?(:class) && !custom_keys.include?(:class)
|
||||
class_xpath = Array(options[:class]).map do |klass|
|
||||
XPath.attr(:class).contains_word(klass)
|
||||
end.reduce(:&)
|
||||
class_xpath = if options[:class].is_a?(XPath::Expression)
|
||||
XPath.attr(:class)[options[:class]]
|
||||
else
|
||||
Array(options[:class]).map do |klass|
|
||||
XPath.attr(:class).contains_word(klass)
|
||||
end.reduce(:&)
|
||||
end
|
||||
expr = "(#{expr})[#{class_xpath}]"
|
||||
end
|
||||
expr
|
||||
end
|
||||
|
||||
def filtered_css(expr)
|
||||
process_id = options.key?(:id) && !custom_keys.include?(:id)
|
||||
process_class = options.key?(:class) && !custom_keys.include?(:class)
|
||||
|
||||
if process_id && options[:id].is_a?(XPath::Expression)
|
||||
raise ArgumentError, "XPath expressions are not supported for the :id filter with CSS based selectors"
|
||||
end
|
||||
if process_class && options[:class].is_a?(XPath::Expression)
|
||||
raise ArgumentError, "XPath expressions are not supported for the :class filter with CSS based selectors"
|
||||
end
|
||||
|
||||
css_selectors = expr.split(',').map(&:rstrip)
|
||||
expr = css_selectors.map do |sel|
|
||||
sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.key?(:id) && !custom_keys.include?(:id)
|
||||
sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}" }.join if options.key?(:class) && !custom_keys.include?(:class)
|
||||
sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if process_id
|
||||
sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}" }.join if process_class
|
||||
sel
|
||||
end.join(", ")
|
||||
expr
|
||||
|
@ -203,8 +233,8 @@ module Capybara
|
|||
def apply_expression_filters(expr)
|
||||
unapplied_options = options.keys - valid_keys
|
||||
expression_filters.inject(expr) do |memo, (name, ef)|
|
||||
if name.is_a?(Regexp)
|
||||
unapplied_options.grep(name).each do |option_name|
|
||||
if ef.matcher?
|
||||
unapplied_options.select { |option_name| ef.handles_option?(option_name) }.each do |option_name|
|
||||
unapplied_options.delete(option_name)
|
||||
memo = ef.apply_filter(memo, option_name, options[option_name])
|
||||
end
|
||||
|
|
|
@ -606,7 +606,7 @@ Capybara.add_selector(:element) do
|
|||
XPath.descendant((locator || '@').to_sym)
|
||||
end
|
||||
|
||||
expression_filter(/.+/) do |xpath, name, val|
|
||||
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
|
||||
case val
|
||||
when Regexp
|
||||
xpath
|
||||
|
@ -617,7 +617,7 @@ Capybara.add_selector(:element) do
|
|||
end
|
||||
end
|
||||
|
||||
filter(/.+/) do |node, name, val|
|
||||
filter(:attributes, matcher: /.+/) do |node, name, val|
|
||||
val.is_a?(Regexp) ? node[name] =~ val : true
|
||||
end
|
||||
|
||||
|
|
|
@ -60,12 +60,12 @@ module Capybara
|
|||
options
|
||||
end
|
||||
|
||||
def add_filter(name, filter_class, *types, **options, &block)
|
||||
def add_filter(name, filter_class, *types, matcher: nil, **options, &block) # rubocop:disable Metrics/ParameterLists
|
||||
types.each { |k| options[k] = true }
|
||||
if filter_class <= Filters::ExpressionFilter
|
||||
@expression_filters[name] = filter_class.new(name, block, options)
|
||||
@expression_filters[name] = filter_class.new(name, matcher, block, options)
|
||||
else
|
||||
@node_filters[name] = filter_class.new(name, block, options)
|
||||
@node_filters[name] = filter_class.new(name, matcher, block, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,9 @@ module Capybara
|
|||
class Selector
|
||||
module Filters
|
||||
class Base
|
||||
def initialize(name, block, **options)
|
||||
def initialize(name, matcher, block, **options)
|
||||
@name = name
|
||||
@matcher = matcher
|
||||
@block = block
|
||||
@options = options
|
||||
@options[:valid_values] = [true, false] if options[:boolean]
|
||||
|
@ -23,6 +24,18 @@ module Capybara
|
|||
@options.key?(:skip_if) && value == @options[:skip_if]
|
||||
end
|
||||
|
||||
def matcher?
|
||||
!@matcher.nil?
|
||||
end
|
||||
|
||||
def handles_option?(option_name)
|
||||
if matcher?
|
||||
option_name =~ @matcher
|
||||
else
|
||||
@name == option_name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply(subject, name, value, skip_value)
|
||||
|
|
|
@ -170,6 +170,7 @@ module Capybara
|
|||
# @option options [Array<>] :valid_values Valid values for this filter
|
||||
# @option options :default The default value of the filter (if any)
|
||||
# @option options :skip_if Value of the filter that will cause it to be skipped
|
||||
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
|
||||
#
|
||||
# If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
|
||||
# is passed for the name the block should accept | node, option_name, option_value |. In either case
|
||||
|
@ -181,11 +182,13 @@ module Capybara
|
|||
#
|
||||
# @overload expression_filter(name, *types, options={}, &block)
|
||||
# @param [Symbol, Regexp] name The filter name
|
||||
# @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
|
||||
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
|
||||
# @param [Hash] options ({}) Options of the filter
|
||||
# @option options [Array<>] :valid_values Valid values for this filter
|
||||
# @option options :default The default value of the filter (if any)
|
||||
# @option options :skip_if Value of the filter that will cause it to be skipped
|
||||
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
|
||||
#
|
||||
# If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
|
||||
# is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
|
||||
|
@ -248,10 +251,10 @@ module Capybara
|
|||
end
|
||||
|
||||
def describe_all_expression_filters(**opts)
|
||||
expression_filters.keys.map do |ef_name|
|
||||
if ef_name.is_a?(Regexp)
|
||||
expression_filters.map do |ef_name, ef|
|
||||
if ef.matcher?
|
||||
opts.keys.map do |k|
|
||||
" with #{k} #{opts[k]}" if k =~ ef_name && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(k)
|
||||
" with #{ef_name}[#{k} => #{opts[k]}]" if ef.handles_option?(k) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(k)
|
||||
end.join
|
||||
elsif opts.key?(ef_name)
|
||||
" with #{ef_name} #{opts[ef_name]}"
|
||||
|
|
|
@ -28,7 +28,7 @@ Capybara::SpecHelper.spec '#assert_matches_selector' do
|
|||
end
|
||||
|
||||
it "should not accept count options" do
|
||||
expect { @element.assert_matches_selector(:css, '.number', count: 1) }.to raise_error(ArgumentError)
|
||||
expect { @element.assert_matches_selector(:css, '.number', count: 1) }.to raise_error(ArgumentError, /count/)
|
||||
end
|
||||
|
||||
it "should accept a filter block" do
|
||||
|
|
|
@ -21,6 +21,9 @@ RSpec.describe Capybara do
|
|||
</div>
|
||||
<div id="#special">
|
||||
</div>
|
||||
<div class="some random words" id="random_words">
|
||||
Something
|
||||
</div>
|
||||
<input id="2checkbox" class="2checkbox" type="checkbox"/>
|
||||
<input type="radio"/>
|
||||
<label for="my_text_input">My Text Input</label>
|
||||
|
@ -37,7 +40,7 @@ RSpec.describe Capybara do
|
|||
</select>
|
||||
<table>
|
||||
<tr><td></td></tr>
|
||||
</table
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
STRING
|
||||
|
@ -52,6 +55,10 @@ RSpec.describe Capybara do
|
|||
Capybara.add_selector :custom_css_selector do
|
||||
css { |selector| selector }
|
||||
end
|
||||
|
||||
Capybara.add_selector :custom_xpath_selector do
|
||||
xpath { |selector| selector }
|
||||
end
|
||||
end
|
||||
|
||||
describe "adding a selector" do
|
||||
|
@ -121,6 +128,16 @@ RSpec.describe Capybara do
|
|||
expect(string.find(:custom_css_selector, "div", id: "#special")[:id]).to eq '#special'
|
||||
expect(string.find(:custom_css_selector, "input", id: "2checkbox")[:id]).to eq '2checkbox'
|
||||
end
|
||||
|
||||
it "accepts XPath expression for xpath based selectors" do
|
||||
expect(string.find(:custom_xpath_selector, './/div', id: XPath.contains('peci'))[:id]).to eq '#special'
|
||||
expect(string.find(:custom_xpath_selector, './/input', id: XPath.ends_with('box'))[:id]).to eq '2checkbox'
|
||||
end
|
||||
|
||||
it "errors XPath expression for CSS based selectors" do
|
||||
expect { string.find(:custom_css_selector, "div", id: XPath.contains('peci')) }
|
||||
.to raise_error(ArgumentError, /not supported/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with :class option" do
|
||||
|
@ -133,6 +150,16 @@ RSpec.describe Capybara do
|
|||
expect(string.find(:custom_css_selector, "input", class: ".special")[:id]).to eq 'file'
|
||||
expect(string.find(:custom_css_selector, "input", class: "2checkbox")[:id]).to eq '2checkbox'
|
||||
end
|
||||
|
||||
it "accepts XPath expression for xpath based selectors" do
|
||||
expect(string.find(:custom_xpath_selector, './/div', class: XPath.contains('dom wor'))[:id]).to eq 'random_words'
|
||||
expect(string.find(:custom_xpath_selector, './/div', class: XPath.ends_with('words'))[:id]).to eq 'random_words'
|
||||
end
|
||||
|
||||
it "errors XPath expression for CSS based selectors" do
|
||||
expect { string.find(:custom_css_selector, "div", class: XPath.contains('random')) }
|
||||
.to raise_error(ArgumentError, /not supported/)
|
||||
end
|
||||
end
|
||||
|
||||
# :css, :xpath, :id, :field, :fieldset, :link, :button, :link_or_button, :fillable_field, :radio_button, :checkbox, :select,
|
||||
|
@ -222,7 +249,7 @@ RSpec.describe Capybara do
|
|||
expect { string.find(:element, 'input', not_there: 'bad', count: 1) }
|
||||
.to(raise_error do |e|
|
||||
expect(e).to be_a(Capybara::ElementNotFound)
|
||||
expect(e.message).to include "not_there bad"
|
||||
expect(e.message).to include "not_there => bad"
|
||||
expect(e.message).not_to include "count 1"
|
||||
end)
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue