Support regexp for system :id and :class filters

This commit is contained in:
Thomas Walpole 2018-10-01 13:15:00 -07:00
parent 15ca8c7d6a
commit 3778898a96
11 changed files with 149 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,12 @@
</label>
</p>
<p>
<label for="customer_other_email">Customer Other Email
<input type="text" name="form[customer_other_email]" value="notben@notben.com" id="customer_other_email"/>
</label>
</p>
<p>
<label for="form_other_title">Other title</label>
<select name="form[other_title]" id="form_other_title">

View File

@ -9,7 +9,12 @@ end
Capybara::SpecHelper.run_specs TestClass.new, 'DSL', capybara_skip: %i[
js modals screenshot frames windows send_keys server hover about_scheme psc download css driver
]
] do |example|
case example.metadata[:full_description]
when /has_css\? should support case insensitive :class and :id options/
pending "Nokogiri doesn't support case insensitive CSS attribute matchers"
end
end
RSpec.describe Capybara::DSL do
after do

View File

@ -19,7 +19,12 @@ skipped_tests = %i[
download
css
]
Capybara::SpecHelper.run_specs TestSessions::RackTest, 'RackTest', capybara_skip: skipped_tests
Capybara::SpecHelper.run_specs TestSessions::RackTest, 'RackTest', capybara_skip: skipped_tests do |example|
case example.metadata[:full_description]
when /has_css\? should support case insensitive :class and :id options/
pending "Nokogiri doesn't support case insensitive CSS attribute matchers"
end
end
RSpec.describe Capybara::Session do # rubocop:disable RSpec/MultipleDescribes
context 'with rack test driver' do

View File

@ -148,6 +148,15 @@ RSpec.describe Capybara do
expect { string.find(:custom_css_selector, 'div', id: XPath.contains('peci')) }
.to raise_error(ArgumentError, /not supported/)
end
it 'accepts Regexp for xpath based selectors' do
expect(string.find(:custom_xpath_selector, './/div', id: /peci/)[:id]).to eq '#special'
expect(string.find(:custom_xpath_selector, './/div', id: /pEcI/i)[:id]).to eq '#special'
end
it 'accepts Regexp for css based selectors' do
expect(string.find(:custom_css_selector, 'div', id: /sp.*al/)[:id]).to eq '#special'
end
end
context 'with :class option' do
@ -177,6 +186,15 @@ RSpec.describe Capybara do
expect { string.find(:custom_css_selector, 'div', class: XPath.contains('random')) }
.to raise_error(ArgumentError, /not supported/)
end
it 'accepts Regexp for XPath based selectors' do
expect(string.find(:custom_xpath_selector, './/div', class: /dom wor/)[:id]).to eq 'random_words'
expect(string.find(:custom_xpath_selector, './/div', class: /dOm WoR/i)[:id]).to eq 'random_words'
end
it 'accepts Regexp for CSS base selectors' do
expect(string.find(:custom_css_selector, 'div', class: /random/)[:id]).to eq 'random_words'
end
end
# :css, :xpath, :id, :field, :fieldset, :link, :button, :link_or_button, :fillable_field, :radio_button, :checkbox, :select,
@ -207,10 +225,14 @@ RSpec.describe Capybara do
expect(string.find(:field, 'form[my_text_input]')[:id]).to eq 'my_text_input'
end
it 'finds by id' do
it 'finds by id string' do
expect(string.find(:field, id: 'my_text_input')[:name]).to eq 'form[my_text_input]'
end
it 'finds by id regexp' do
expect(string.find(:field, id: /my_text_inp/)[:name]).to eq 'form[my_text_input]'
end
it 'finds by name' do
expect(string.find(:field, name: 'form[my_text_input]')[:id]).to eq 'my_text_input'
end