Allow specification of expression filters outside the main selector expression

This commit is contained in:
Thomas Walpole 2016-10-05 15:16:00 -07:00
parent add7ae24e9
commit d4eae5cdf4
7 changed files with 184 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

28
spec/filter_set_spec.rb Normal file
View File

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

View File

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