From 34994803816ee7cbbbec795d9936784393f3578f Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Mon, 1 Oct 2018 18:02:26 -0700 Subject: [PATCH] Move basic selector condition generation into builder classes --- lib/capybara/queries/selector_query.rb | 71 +++---------------- lib/capybara/selector.rb | 30 ++------ lib/capybara/selector/builders/css_builder.rb | 49 +++++++++++++ .../selector/builders/xpath_builder.rb | 56 +++++++++++++++ lib/capybara/selector/regexp_disassembler.rb | 1 + lib/capybara/selector/selector.rb | 22 +++--- 6 files changed, 134 insertions(+), 95 deletions(-) create mode 100644 lib/capybara/selector/builders/css_builder.rb create mode 100644 lib/capybara/selector/builders/xpath_builder.rb diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index 9596281d..5743c857 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -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 diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index 67d988b5..b5bd5633 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -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| diff --git a/lib/capybara/selector/builders/css_builder.rb b/lib/capybara/selector/builders/css_builder.rb new file mode 100644 index 00000000..ac1d0d7f --- /dev/null +++ b/lib/capybara/selector/builders/css_builder.rb @@ -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 diff --git a/lib/capybara/selector/builders/xpath_builder.rb b/lib/capybara/selector/builders/xpath_builder.rb new file mode 100644 index 00000000..82e083f7 --- /dev/null +++ b/lib/capybara/selector/builders/xpath_builder.rb @@ -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 diff --git a/lib/capybara/selector/regexp_disassembler.rb b/lib/capybara/selector/regexp_disassembler.rb index fee6f6df..3dbb73ad 100644 --- a/lib/capybara/selector/regexp_disassembler.rb +++ b/lib/capybara/selector/regexp_disassembler.rb @@ -2,6 +2,7 @@ module Capybara class Selector + # @api private class RegexpDisassembler def initialize(regexp) @regexp = regexp diff --git a/lib/capybara/selector/selector.rb b/lib/capybara/selector/selector.rb index 3e187d49..579cedbf 100644 --- a/lib/capybara/selector/selector.rb +++ b/lib/capybara/selector/selector.rb @@ -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