diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index f80d1a08..9596281d 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -61,6 +61,7 @@ module Capybara return true if (@resolved_node&.== node) && options[:allow_self] @applied_filters ||= :system + return false unless matches_id_filter?(node) && matches_class_filter?(node) return false unless matches_text_filter?(node) && matches_exact_text_filter?(node) && matches_visible_filter?(node) @applied_filters = :node @@ -221,14 +222,7 @@ module Capybara end def filtered_xpath(expr) - if use_default_id_filter? - id_xpath = if options[:id].is_a? XPath::Expression - XPath.attr(:id)[options[:id]] - else - XPath.attr(:id) == options[:id] - end - expr = "(#{expr})[#{id_xpath}]" - end + expr = "(#{expr})[#{xpath_from_id}]" if use_default_id_filter? expr = "(#{expr})[#{xpath_from_classes}]" if use_default_class_filter? expr end @@ -250,33 +244,58 @@ module Capybara end def css_from_classes - if options[:class].is_a?(XPath::Expression) + 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 - - 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 css_from_id - if options[:id].is_a?(XPath::Expression) + 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 - "##{::Capybara::Selector::CSS.escape(options[:id])}" + 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 - return XPath.attr(:class)[options[:class]] if options[:class].is_a?(XPath::Expression) - - 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(:&) + 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 end def apply_expression_filters(expression) @@ -320,6 +339,18 @@ module Capybara node.is_a?(::Capybara::Node::Simple) && node.path == '/' end + def matches_id_filter?(node) + return true unless use_default_id_filter? && options[:id].is_a?(Regexp) + + node[:id] =~ options[:id] + end + + def matches_class_filter?(node) + return true unless use_default_class_filter? && options[:class].is_a?(Regexp) + + node[:class] =~ options[:class] + end + def matches_text_filter?(node) value = options[:text] return true unless value @@ -357,6 +388,14 @@ module Capybara text_visible = :all if text_visible == :hidden !!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(:&) + end end end end diff --git a/lib/capybara/rack_test/driver.rb b/lib/capybara/rack_test/driver.rb index 68c535a9..f7ca1ad0 100644 --- a/lib/capybara/rack_test/driver.rb +++ b/lib/capybara/rack_test/driver.rb @@ -75,6 +75,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base def find_css(selector) browser.find(:css, selector) + rescue Nokogiri::CSS::SyntaxError + raise unless selector.include?(' i]') + + raise ArgumentError, "This driver doesn't support case insensitive attribute matching when using CSS base selectors" end def html diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index 4df2be83..67d988b5 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -97,7 +97,7 @@ Capybara.add_selector(:link) do when true XPath.attr(:href) when Regexp - XPath.attr(:href)[regexp_to_conditions(href)] + XPath.attr(:href)[regexp_to_xpath_conditions(href)] else XPath.attr(:href) == href.to_s end @@ -138,7 +138,7 @@ Capybara.add_selector(:link) do if (href = options[:href]) if !href.is_a?(Regexp) desc << " with href #{href.inspect}" - elsif regexp_to_conditions(href) + elsif regexp_to_xpath_conditions(href) desc << " with href matching #{href.inspect}" end end @@ -147,7 +147,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_conditions(href).nil? + " with href matching #{href.inspect}" if href.is_a?(Regexp) && regexp_to_xpath_conditions(href).nil? end end @@ -483,7 +483,7 @@ Capybara.add_selector(:element) do expression_filter(:attributes, matcher: /.+/) do |xpath, name, val| case val when Regexp - xpath[XPath.attr(name)[regexp_to_conditions(val)]] + xpath[XPath.attr(name)[regexp_to_xpath_conditions(val)]] when true xpath[XPath.attr(name)] when false diff --git a/lib/capybara/selector/regexp_disassembler.rb b/lib/capybara/selector/regexp_disassembler.rb index 914ceb5c..fee6f6df 100644 --- a/lib/capybara/selector/regexp_disassembler.rb +++ b/lib/capybara/selector/regexp_disassembler.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'xpath' - module Capybara class Selector class RegexpDisassembler @@ -10,14 +8,6 @@ module Capybara @regexp_source = regexp.source end - def conditions - condition = XPath.current - condition = condition.uppercase if @regexp.casefold? - substrings.map do |str| - condition.contains(@regexp.casefold? ? str.upcase : str) - end.reduce(:&) - end - def substrings @substrings ||= begin source = @regexp_source.dup @@ -37,7 +27,9 @@ module Capybara end return [] if source.include?('|') # can't handle alternation here - source.match(/\A\^?(.*?)\$?\Z/).captures[0].split('.').reject(&:empty?).uniq + strs = source.match(/\A\^?(.*?)\$?\Z/).captures[0].split('.').reject(&:empty?).uniq + strs = strs.map(&:upcase) if @regexp.casefold? + strs end end diff --git a/lib/capybara/selector/selector.rb b/lib/capybara/selector/selector.rb index 438dc426..3e187d49 100644 --- a/lib/capybara/selector/selector.rb +++ b/lib/capybara/selector/selector.rb @@ -446,8 +446,12 @@ module Capybara Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&) end - def regexp_to_conditions(regexp) - RegexpDisassembler.new(regexp).conditions + 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 diff --git a/lib/capybara/spec/session/has_css_spec.rb b/lib/capybara/spec/session/has_css_spec.rb index 40730841..b89c52c5 100644 --- a/lib/capybara/spec/session/has_css_spec.rb +++ b/lib/capybara/spec/session/has_css_spec.rb @@ -23,6 +23,22 @@ Capybara::SpecHelper.spec '#has_css?' do expect(@session).not_to have_css('p.nosuchclass') end + it 'should support :id option' do + expect(@session).to have_css('h2', id: 'h2one') + expect(@session).to have_css('h2') + expect(@session).to have_css('h2', id: /h2o/) + end + + it 'should support :class option' do + expect(@session).to have_css('li', class: 'guitarist') + expect(@session).to have_css('li', class: /guitar/) + end + + it 'should support case insensitive :class and :id options' do + expect(@session).to have_css('li', class: /UiTaRI/i) + expect(@session).to have_css('h2', id: /2ON/i) + end + it 'should respect scopes' do @session.within "//p[@id='first']" do expect(@session).to have_css('a#foo') diff --git a/lib/capybara/spec/session/selectors_spec.rb b/lib/capybara/spec/session/selectors_spec.rb index 359b34e5..88328abc 100644 --- a/lib/capybara/spec/session/selectors_spec.rb +++ b/lib/capybara/spec/session/selectors_spec.rb @@ -40,8 +40,18 @@ Capybara::SpecHelper.spec Capybara::Selector do end describe 'field selectors' do - it 'can find specifically by id' do - expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com' + context 'with :id option' do + it 'can find specifically by id' do + expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com' + end + + it 'can find by regex' do + expect(@session.find(:field, id: /ustomer.emai/).value).to eq 'ben@ben.com' + end + + it 'can find by case-insensitive id' do + expect(@session.find(:field, id: /StOmer.emAI/i).value).to eq 'ben@ben.com' + end end it 'can find specifically by name' do diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb index afac5035..378d3179 100644 --- a/lib/capybara/spec/views/form.erb +++ b/lib/capybara/spec/views/form.erb @@ -25,6 +25,12 @@

+

+ +

+