Allow specification of expression filters outside the main selector expression
This commit is contained in:
parent
add7ae24e9
commit
d4eae5cdf4
|
@ -81,7 +81,7 @@ module Capybara
|
|||
when :hidden then return false if node.visible?
|
||||
end
|
||||
|
||||
res = query_filters.all? do |name, filter|
|
||||
res = node_filters.all? do |name, filter|
|
||||
if options.has_key?(name)
|
||||
filter.matches?(node, options[name])
|
||||
elsif filter.default?
|
||||
|
@ -114,16 +114,17 @@ module Capybara
|
|||
|
||||
def xpath(exact=nil)
|
||||
exact = self.exact? if exact.nil?
|
||||
expr = if @expression.respond_to?(:to_xpath) and exact
|
||||
@expression.to_xpath(:exact)
|
||||
expr = apply_expression_filters(@expression)
|
||||
expr = if expr.respond_to?(:to_xpath) and exact
|
||||
expr.to_xpath(:exact)
|
||||
else
|
||||
@expression.to_s
|
||||
expr.to_s
|
||||
end
|
||||
filtered_xpath(expr)
|
||||
end
|
||||
|
||||
def css
|
||||
filtered_css(@expression)
|
||||
filtered_css(apply_expression_filters(@expression))
|
||||
end
|
||||
|
||||
# @api private
|
||||
|
@ -155,16 +156,22 @@ module Capybara
|
|||
VALID_KEYS + custom_keys
|
||||
end
|
||||
|
||||
def query_filters
|
||||
def node_filters
|
||||
if options.has_key?(:filter_set)
|
||||
Capybara::Selector::FilterSet.all[options[:filter_set]].filters
|
||||
::Capybara::Selector::FilterSet.all[options[:filter_set]].node_filters
|
||||
else
|
||||
@selector.custom_filters
|
||||
@selector.node_filters
|
||||
end
|
||||
end
|
||||
|
||||
def expression_filters
|
||||
filters = @selector.expression_filters
|
||||
filters.merge ::Capybara::Selector::FilterSet.all[options[:filter_set]].expression_filters if options.has_key?(:filter_set)
|
||||
filters
|
||||
end
|
||||
|
||||
def custom_keys
|
||||
@custom_keys ||= query_filters.keys + @selector.expression_filters
|
||||
@custom_keys ||= node_filters.keys + expression_filters.keys
|
||||
end
|
||||
|
||||
def assert_valid_keys
|
||||
|
@ -200,6 +207,18 @@ module Capybara
|
|||
expr
|
||||
end
|
||||
|
||||
def apply_expression_filters(expr)
|
||||
expression_filters.inject(expr) do |memo, (name, ef)|
|
||||
if options.has_key?(name)
|
||||
ef.apply_filter(memo, options[name])
|
||||
elsif ef.default?
|
||||
ef.apply_filter(memo, ef.default)
|
||||
else
|
||||
memo
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def warn_exact_usage
|
||||
if options.has_key?(:exact) && !supports_exact?
|
||||
warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression.to_s}\" has no effect."
|
||||
|
|
|
@ -6,6 +6,9 @@ Capybara::Selector::FilterSet.add(:_field) do
|
|||
filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
|
||||
filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
|
||||
|
||||
expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name).equals(val)] }
|
||||
expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder).equals(val)] }
|
||||
|
||||
describe do |options|
|
||||
desc, states = String.new, []
|
||||
states << 'checked' if options[:checked] || (options[:unchecked] == false)
|
||||
|
@ -65,21 +68,21 @@ end
|
|||
# @filter [Boolean] :disabled Match disabled field?
|
||||
# @filter [Boolean] :multiple Match fields that accept multiple values
|
||||
Capybara.add_selector(:field) do
|
||||
xpath(:name, :placeholder, :type) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
|
||||
if options[:type]
|
||||
type=options[:type].to_s
|
||||
if ['textarea', 'select'].include?(type)
|
||||
xpath = XPath.descendant(type.to_sym)
|
||||
else
|
||||
xpath = xpath[XPath.attr(:type).equals(type)]
|
||||
end
|
||||
end
|
||||
xpath=locate_field(xpath, locator, options)
|
||||
xpath
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field) # checked/unchecked/disabled/multiple
|
||||
expression_filter(:type) do |expr, type|
|
||||
type = type.to_s
|
||||
if ['textarea', 'select'].include?(type)
|
||||
expr.axis(:self, type.to_sym)
|
||||
else
|
||||
expr[XPath.attr(:type).equals(type)]
|
||||
end
|
||||
end
|
||||
|
||||
filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
|
||||
|
||||
filter(:readonly, :boolean) { |node, value| not(value ^ node.readonly?) }
|
||||
filter(:with) do |node, with|
|
||||
|
@ -87,7 +90,7 @@ Capybara.add_selector(:field) do
|
|||
end
|
||||
describe do |options|
|
||||
desc = String.new
|
||||
(expression_filters - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.has_key?(ef) }
|
||||
(expression_filters.keys - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.has_key?(ef) }
|
||||
desc << " of type #{options[:type].inspect}" if options[:type]
|
||||
desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
|
||||
desc
|
||||
|
@ -198,7 +201,7 @@ Capybara.add_selector(:button) do
|
|||
|
||||
res_xpath = input_btn_xpath + btn_xpath + image_btn_xpath
|
||||
|
||||
res_xpath = expression_filters.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
res_xpath = expression_filters.keys.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
|
||||
res_xpath
|
||||
end
|
||||
|
@ -244,20 +247,21 @@ end
|
|||
#
|
||||
Capybara.add_selector(:fillable_field) do
|
||||
label "field"
|
||||
xpath(:name, :placeholder, :type) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
|
||||
if options[:type]
|
||||
type=options[:type].to_s
|
||||
if ['textarea'].include?(type)
|
||||
xpath = XPath.descendant(type.to_sym)
|
||||
else
|
||||
xpath = xpath[XPath.attr(:type).equals(type)]
|
||||
end
|
||||
end
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field, [:disabled, :multiple])
|
||||
expression_filter(:type) do |expr, type|
|
||||
type = type.to_s
|
||||
if ['textarea'].include?(type)
|
||||
expr.axis(:self, type.to_sym)
|
||||
else
|
||||
expr[XPath.attr(:type).equals(type)]
|
||||
end
|
||||
end
|
||||
|
||||
filter_set(:_field, [:disabled, :multiple, :name, :placeholder])
|
||||
|
||||
filter(:with) do |node, with|
|
||||
with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
|
||||
|
@ -286,12 +290,12 @@ end
|
|||
#
|
||||
Capybara.add_selector(:radio_button) do
|
||||
label "radio button"
|
||||
xpath(:name) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field, [:checked, :unchecked, :disabled])
|
||||
filter_set(:_field, [:checked, :unchecked, :disabled, :name])
|
||||
|
||||
filter(:option) { |node, value| node.value == value.to_s }
|
||||
|
||||
|
@ -317,12 +321,12 @@ end
|
|||
# @filter [String] :option Match the value
|
||||
#
|
||||
Capybara.add_selector(:checkbox) do
|
||||
xpath(:name) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field, [:checked, :unchecked, :disabled])
|
||||
filter_set(:_field, [:checked, :unchecked, :disabled, :name])
|
||||
|
||||
filter(:option) { |node, value| node.value == value.to_s }
|
||||
|
||||
|
@ -351,12 +355,12 @@ end
|
|||
#
|
||||
Capybara.add_selector(:select) do
|
||||
label "select box"
|
||||
xpath(:name, :placeholder) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:select)
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field, [:disabled, :multiple])
|
||||
filter_set(:_field, [:disabled, :multiple, :name, :placeholder])
|
||||
|
||||
filter(:options) do |node, options|
|
||||
if node.visible?
|
||||
|
@ -429,12 +433,12 @@ end
|
|||
#
|
||||
Capybara.add_selector(:file_field) do
|
||||
label "file field"
|
||||
xpath(:name) do |locator, options|
|
||||
xpath do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
||||
filter_set(:_field, [:disabled, :multiple])
|
||||
filter_set(:_field, [:disabled, :multiple, :name])
|
||||
|
||||
describe do |options|
|
||||
desc = String.new
|
||||
|
@ -518,7 +522,7 @@ Capybara.add_selector(:frame) do
|
|||
xpath(:name) do |locator, options|
|
||||
xpath = XPath.descendant(:iframe) + XPath.descendant(:frame)
|
||||
xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.attr(:name).equals(locator)] unless locator.nil?
|
||||
xpath = expression_filters.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
xpath = expression_filters.keys.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
xpath
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
require 'capybara/selector/filter'
|
||||
|
||||
module Capybara
|
||||
class Selector
|
||||
class ExpressionFilter < Filter
|
||||
undef_method :matches?
|
||||
|
||||
def apply_filter(expr, value)
|
||||
return expr if skip?(value)
|
||||
|
||||
if !valid_value?(value)
|
||||
msg = "Invalid value #{value.inspect} passed to expression filter #{@name} - "
|
||||
if default?
|
||||
warn msg + "defaulting to #{default}"
|
||||
value = default
|
||||
else
|
||||
warn msg + "skipping"
|
||||
return expr
|
||||
end
|
||||
end
|
||||
|
||||
@block.call(expr, value)
|
||||
end
|
||||
end
|
||||
|
||||
class IdentityExpressionFilter < ExpressionFilter
|
||||
def initialize
|
||||
end
|
||||
|
||||
def default?
|
||||
false
|
||||
end
|
||||
|
||||
def apply_filter(expr, _value)
|
||||
return expr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,9 +13,11 @@ module Capybara
|
|||
end
|
||||
|
||||
def filter(name, *types_and_options, &block)
|
||||
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
|
||||
types_and_options.each { |k| options[k] = true}
|
||||
filters[name] = Filter.new(name, block, options)
|
||||
add_filter(name, Filter, *types_and_options, &block)
|
||||
end
|
||||
|
||||
def expression_filter(name, *types_and_options, &block)
|
||||
add_filter(name, ExpressionFilter, *types_and_options, &block)
|
||||
end
|
||||
|
||||
def describe(&block)
|
||||
|
@ -30,7 +32,16 @@ module Capybara
|
|||
@filters ||= {}
|
||||
end
|
||||
|
||||
def node_filters
|
||||
filters.reject { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
|
||||
end
|
||||
|
||||
def expression_filters
|
||||
filters.select { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
def all
|
||||
@filter_sets ||= {}
|
||||
end
|
||||
|
@ -43,6 +54,14 @@ module Capybara
|
|||
all.delete(name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_filter(name, filter_class, *types_and_options, &block)
|
||||
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
|
||||
types_and_options.each { |k| options[k] = true}
|
||||
filters[name] = filter_class.new(name, block, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
require 'capybara/selector/expression_filter'
|
||||
require 'capybara/selector/filter_set'
|
||||
require 'capybara/selector/css'
|
||||
require 'xpath'
|
||||
|
@ -21,7 +22,7 @@ end
|
|||
module Capybara
|
||||
class Selector
|
||||
|
||||
attr_reader :name, :format, :expression_filters
|
||||
attr_reader :name, :format
|
||||
|
||||
class << self
|
||||
def all
|
||||
|
@ -50,7 +51,7 @@ module Capybara
|
|||
@description = nil
|
||||
@format = nil
|
||||
@expression = nil
|
||||
@expression_filters = []
|
||||
@expression_filters = {}
|
||||
@default_visibility = nil
|
||||
instance_eval(&block)
|
||||
end
|
||||
|
@ -59,6 +60,14 @@ module Capybara
|
|||
@filter_set.filters
|
||||
end
|
||||
|
||||
def node_filters
|
||||
@filter_set.node_filters
|
||||
end
|
||||
|
||||
def expression_filters
|
||||
@filter_set.expression_filters
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Define a selector by an xpath expression
|
||||
|
@ -74,7 +83,10 @@ module Capybara
|
|||
# @return [#call] The block that will be called to generate the XPath expression
|
||||
#
|
||||
def xpath(*expression_filters, &block)
|
||||
@format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
|
||||
if block
|
||||
@format, @expression = :xpath, block
|
||||
expression_filters.flatten.each { |ef| custom_filters[ef] = IdentityExpressionFilter.new }
|
||||
end
|
||||
format == :xpath ? @expression : nil
|
||||
end
|
||||
|
||||
|
@ -93,7 +105,10 @@ module Capybara
|
|||
# @return [#call] The block that will be called to generate the CSS selector
|
||||
#
|
||||
def css(*expression_filters, &block)
|
||||
@format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
|
||||
if block
|
||||
@format, @expression = :css, block
|
||||
expression_filters.flatten.each { |ef| custom_filters[ef] = nil }
|
||||
end
|
||||
format == :css ? @expression : nil
|
||||
end
|
||||
|
||||
|
@ -172,10 +187,16 @@ module Capybara
|
|||
#
|
||||
def filter(name, *types_and_options, &block)
|
||||
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
|
||||
types_and_options.each { |k| options[k] = true}
|
||||
types_and_options.each { |k| options[k] = true }
|
||||
custom_filters[name] = Filter.new(name, block, options)
|
||||
end
|
||||
|
||||
def expression_filter(name, *types_and_options, &block)
|
||||
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
|
||||
types_and_options.each { |k| options[k] = true }
|
||||
custom_filters[name] = ExpressionFilter.new(name, block, options)
|
||||
end
|
||||
|
||||
def filter_set(name, filters_to_use = nil)
|
||||
f_set = FilterSet.all[name]
|
||||
f_set.filters.each do |n, filter|
|
||||
|
@ -225,7 +246,7 @@ module Capybara
|
|||
locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
|
||||
end
|
||||
|
||||
locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
# locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
locate_xpath
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Capybara::Selector::FilterSet do
|
||||
after do
|
||||
Capybara::Selector::FilterSet.remove(:test)
|
||||
end
|
||||
|
||||
it "allows node filters" do
|
||||
fs = Capybara::Selector::FilterSet.add(:test) do
|
||||
filter(:node_test, :boolean) { |node, value| true }
|
||||
expression_filter(:expression_test, :boolean) { |expr, value| true }
|
||||
end
|
||||
|
||||
expect(fs.node_filters.keys).to include(:node_test)
|
||||
expect(fs.node_filters.keys).not_to include(:expression_test)
|
||||
end
|
||||
|
||||
it "allows expression filters" do
|
||||
fs = Capybara::Selector::FilterSet.add(:test) do
|
||||
filter(:node_test, :boolean) { |node, value| true }
|
||||
expression_filter(:expression_test, :boolean) { |expr, value| true }
|
||||
end
|
||||
|
||||
expect(fs.expression_filters.keys).to include(:expression_test)
|
||||
expect(fs.expression_filters.keys).not_to include(:node_test)
|
||||
end
|
||||
end
|
|
@ -28,7 +28,7 @@ RSpec.describe Capybara do
|
|||
<input type="hidden" id="hidden_field" value="this is hidden"/>
|
||||
<a href="#">link</a>
|
||||
<fieldset></fieldset>
|
||||
<select>
|
||||
<select id="select">
|
||||
<option value="a">A</option>
|
||||
<option value="b" disabled>B</option>
|
||||
<option value="c" selected>C</option>
|
||||
|
@ -177,6 +177,7 @@ RSpec.describe Capybara do
|
|||
|
||||
it "finds by type" do
|
||||
expect(string.find(:field, type: 'file')[:id]).to eq 'file'
|
||||
expect(string.find(:field, type: 'select')[:id]).to eq 'select'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue