Move basic selector condition generation into builder classes

This commit is contained in:
Thomas Walpole 2018-10-01 18:02:26 -07:00
parent 3778898a96
commit 3499480381
6 changed files with 134 additions and 95 deletions

View File

@ -222,15 +222,15 @@ module Capybara
end
def filtered_xpath(expr)
expr = "(#{expr})[#{xpath_from_id}]" if use_default_id_filter?
expr = "(#{expr})[#{xpath_from_classes}]" if use_default_class_filter?
expr = "(#{expr})[#{conditions_from_id}]" if use_default_id_filter?
expr = "(#{expr})[#{conditions_from_classes}]" if use_default_class_filter?
expr
end
def filtered_css(expr)
::Capybara::Selector::CSS.split(expr).map do |sel|
sel += css_from_id if use_default_id_filter?
sel += css_from_classes if use_default_class_filter?
sel += conditions_from_id if use_default_id_filter?
sel += conditions_from_classes if use_default_class_filter?
sel
end.join(', ')
end
@ -243,59 +243,12 @@ module Capybara
options.key?(:class) && !custom_keys.include?(:class)
end
def css_from_classes
case options[:class]
when XPath::Expression
raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
when Regexp
strs = Selector::RegexpDisassembler.new(options[:class]).substrings
strs.map { |str| "[class*='#{str}'#{' i' if options[:class].casefold?}]" }.join
else
classes = Array(options[:class]).group_by { |cl| cl.start_with? '!' }
(classes[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
classes[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
end
def conditions_from_classes
builder.class_conditions(options[:class])
end
def css_from_id
case options[:id]
when XPath::Expression
raise ArgumentError, 'XPath expressions are not supported for the :id filter with CSS based selectors'
when Regexp
Selector::RegexpDisassembler.new(options[:id]).substrings.map do |str|
"[id*='#{str}'#{' i' if options[:id].casefold?}]"
end.join
else
"##{::Capybara::Selector::CSS.escape(options[:id])}"
end
end
def xpath_from_id
case options[:id]
when XPath::Expression
XPath.attr(:id)[options[:id]]
when Regexp
XPath.attr(:id)[regexp_to_xpath_conditions(options[:id])]
else
XPath.attr(:id) == options[:id]
end
end
def xpath_from_classes
case options[:class]
when XPath::Expression
XPath.attr(:class)[options[:class]]
when Regexp
XPath.attr(:class)[regexp_to_xpath_conditions(options[:class])]
else
Array(options[:class]).map do |klass|
if klass.start_with?('!')
!XPath.attr(:class).contains_word(klass.slice(1..-1))
else
XPath.attr(:class).contains_word(klass)
end
end.reduce(:&)
end
def conditions_from_id
builder.attribute_conditions(id: options[:id])
end
def apply_expression_filters(expression)
@ -389,12 +342,8 @@ module Capybara
!!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
end
def regexp_to_xpath_conditions(regexp)
condition = XPath.current
condition = condition.uppercase if regexp.casefold?
Selector::RegexpDisassembler.new(regexp).substrings.map do |str|
condition.contains(str)
end.reduce(:&)
def builder
selector.builder
end
end
end

View File

@ -90,18 +90,7 @@ end
Capybara.add_selector(:link) do
xpath(:title, :alt) do |locator, href: true, alt: nil, title: nil, **|
xpath = XPath.descendant(:a)
xpath = xpath[
case href
when nil, false
!XPath.attr(:href)
when true
XPath.attr(:href)
when Regexp
XPath.attr(:href)[regexp_to_xpath_conditions(href)]
else
XPath.attr(:href) == href.to_s
end
]
xpath = xpath[@href_conditions = builder.attribute_conditions(href: href)]
unless locator.nil?
locator = locator.to_s
@ -138,7 +127,7 @@ Capybara.add_selector(:link) do
if (href = options[:href])
if !href.is_a?(Regexp)
desc << " with href #{href.inspect}"
elsif regexp_to_xpath_conditions(href)
elsif @href_conditions
desc << " with href matching #{href.inspect}"
end
end
@ -147,7 +136,7 @@ Capybara.add_selector(:link) do
end
describe_node_filters do |href: nil, **|
" with href matching #{href.inspect}" if href.is_a?(Regexp) && regexp_to_xpath_conditions(href).nil?
" with href matching #{href.inspect}" if href.is_a?(Regexp) && @href_conditions.nil?
end
end
@ -481,18 +470,7 @@ Capybara.add_selector(:element) do
end
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
case val
when Regexp
xpath[XPath.attr(name)[regexp_to_xpath_conditions(val)]]
when true
xpath[XPath.attr(name)]
when false
xpath[!XPath.attr(name)]
when XPath::Expression
xpath[XPath.attr(name)[val]]
else
xpath[XPath.attr(name.to_sym) == val]
end
xpath[builder.attribute_conditions(name => val)]
end
node_filter(:attributes, matcher: /.+/) do |node, name, val|

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'xpath'
module Capybara
class Selector
# @api private
class CSSBuilder
class << self
def attribute_conditions(attributes)
attributes.map do |attribute, value|
case value
when XPath::Expression
raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
when Regexp
Selector::RegexpDisassembler.new(value).substrings.map do |str|
"[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
end.join
when true
"[#{attribute}]"
when false
':not([attribute])'
else
if attribute == :id
"##{::Capybara::Selector::CSS.escape(value)}"
else
"[#{attribute}='#{value}']"
end
end
end.join
end
def class_conditions(classes)
case classes
when XPath::Expression
raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
when Regexp
strs = Selector::RegexpDisassembler.new(classes).substrings
strs.map { |str| "[class*='#{str}'#{' i' if classes.casefold?}]" }.join
else
cls = Array(classes).group_by { |cl| cl.start_with? '!' }
(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
end
end
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'xpath'
module Capybara
class Selector
# @api private
class XPathBuilder
class << self
def attribute_conditions(attributes)
attributes.map do |attribute, value|
case value
when XPath::Expression
XPath.attr(attribute)[value]
when Regexp
XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
when true
XPath.attr(attribute)
when false, nil
!XPath.attr(attribute)
else
XPath.attr(attribute) == value.to_s
end
end.reduce(:&)
end
def class_conditions(classes)
case classes
when XPath::Expression
XPath.attr(:class)[classes]
when Regexp
XPath.attr(:class)[regexp_to_xpath_conditions(classes)]
else
Array(classes).map do |klass|
if klass.start_with?('!')
!XPath.attr(:class).contains_word(klass.slice(1..-1))
else
XPath.attr(:class).contains_word(klass)
end
end.reduce(:&)
end
end
private
def regexp_to_xpath_conditions(regexp)
condition = XPath.current
condition = condition.uppercase if regexp.casefold?
Selector::RegexpDisassembler.new(regexp).substrings.map do |str|
condition.contains(str)
end.reduce(:&)
end
end
end
end
end

View File

@ -2,6 +2,7 @@
module Capybara
class Selector
# @api private
class RegexpDisassembler
def initialize(regexp)
@regexp = regexp

View File

@ -5,6 +5,8 @@
require 'capybara/selector/filter_set'
require 'capybara/selector/css'
require 'capybara/selector/regexp_disassembler'
require 'capybara/selector/builders/xpath_builder'
require 'capybara/selector/builders/css_builder'
module Capybara
#
@ -395,6 +397,18 @@ module Capybara
vis.nil? ? fallback : vis
end
# @api private
def builder
case format
when :css
Capybara::Selector::CSSBuilder
when :xpath
Capybara::Selector::XPathBuilder
else
raise NotImplementedError, "No builder exists for selector of type #{format}"
end
end
private
def enable_aria_label
@ -445,14 +459,6 @@ module Capybara
def find_by_class_attr(classes)
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
end
def regexp_to_xpath_conditions(regexp)
condition = XPath.current
condition = condition.uppercase if regexp.casefold?
RegexpDisassembler.new(regexp).substrings.map do |str|
condition.contains(str)
end.reduce(:&)
end
end
end