Support regexp for system :id and :class filters
This commit is contained in:
parent
15ca8c7d6a
commit
3778898a96
|
@ -61,6 +61,7 @@ module Capybara
|
||||||
return true if (@resolved_node&.== node) && options[:allow_self]
|
return true if (@resolved_node&.== node) && options[:allow_self]
|
||||||
|
|
||||||
@applied_filters ||= :system
|
@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)
|
return false unless matches_text_filter?(node) && matches_exact_text_filter?(node) && matches_visible_filter?(node)
|
||||||
|
|
||||||
@applied_filters = :node
|
@applied_filters = :node
|
||||||
|
@ -221,14 +222,7 @@ module Capybara
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_xpath(expr)
|
def filtered_xpath(expr)
|
||||||
if use_default_id_filter?
|
expr = "(#{expr})[#{xpath_from_id}]" 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_classes}]" if use_default_class_filter?
|
expr = "(#{expr})[#{xpath_from_classes}]" if use_default_class_filter?
|
||||||
expr
|
expr
|
||||||
end
|
end
|
||||||
|
@ -250,26 +244,50 @@ module Capybara
|
||||||
end
|
end
|
||||||
|
|
||||||
def css_from_classes
|
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'
|
raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
|
||||||
end
|
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 = Array(options[:class]).group_by { |cl| cl.start_with? '!' }
|
||||||
(classes[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
|
(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
|
classes[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
|
||||||
end
|
end
|
||||||
|
|
||||||
def css_from_id
|
|
||||||
if options[:id].is_a?(XPath::Expression)
|
|
||||||
raise ArgumentError, 'XPath expressions are not supported for the :id filter with CSS based selectors'
|
|
||||||
end
|
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])}"
|
"##{::Capybara::Selector::CSS.escape(options[:id])}"
|
||||||
end
|
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
|
def xpath_from_classes
|
||||||
return XPath.attr(:class)[options[:class]] if options[:class].is_a?(XPath::Expression)
|
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|
|
Array(options[:class]).map do |klass|
|
||||||
if klass.start_with?('!')
|
if klass.start_with?('!')
|
||||||
!XPath.attr(:class).contains_word(klass.slice(1..-1))
|
!XPath.attr(:class).contains_word(klass.slice(1..-1))
|
||||||
|
@ -278,6 +296,7 @@ module Capybara
|
||||||
end
|
end
|
||||||
end.reduce(:&)
|
end.reduce(:&)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def apply_expression_filters(expression)
|
def apply_expression_filters(expression)
|
||||||
unapplied_options = options.keys - valid_keys
|
unapplied_options = options.keys - valid_keys
|
||||||
|
@ -320,6 +339,18 @@ module Capybara
|
||||||
node.is_a?(::Capybara::Node::Simple) && node.path == '/'
|
node.is_a?(::Capybara::Node::Simple) && node.path == '/'
|
||||||
end
|
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)
|
def matches_text_filter?(node)
|
||||||
value = options[:text]
|
value = options[:text]
|
||||||
return true unless value
|
return true unless value
|
||||||
|
@ -357,6 +388,14 @@ module Capybara
|
||||||
text_visible = :all if text_visible == :hidden
|
text_visible = :all if text_visible == :hidden
|
||||||
!!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
|
!!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -75,6 +75,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
|
||||||
|
|
||||||
def find_css(selector)
|
def find_css(selector)
|
||||||
browser.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
|
end
|
||||||
|
|
||||||
def html
|
def html
|
||||||
|
|
|
@ -97,7 +97,7 @@ Capybara.add_selector(:link) do
|
||||||
when true
|
when true
|
||||||
XPath.attr(:href)
|
XPath.attr(:href)
|
||||||
when Regexp
|
when Regexp
|
||||||
XPath.attr(:href)[regexp_to_conditions(href)]
|
XPath.attr(:href)[regexp_to_xpath_conditions(href)]
|
||||||
else
|
else
|
||||||
XPath.attr(:href) == href.to_s
|
XPath.attr(:href) == href.to_s
|
||||||
end
|
end
|
||||||
|
@ -138,7 +138,7 @@ Capybara.add_selector(:link) do
|
||||||
if (href = options[:href])
|
if (href = options[:href])
|
||||||
if !href.is_a?(Regexp)
|
if !href.is_a?(Regexp)
|
||||||
desc << " with href #{href.inspect}"
|
desc << " with href #{href.inspect}"
|
||||||
elsif regexp_to_conditions(href)
|
elsif regexp_to_xpath_conditions(href)
|
||||||
desc << " with href matching #{href.inspect}"
|
desc << " with href matching #{href.inspect}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -147,7 +147,7 @@ Capybara.add_selector(:link) do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe_node_filters do |href: nil, **|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -483,7 +483,7 @@ Capybara.add_selector(:element) do
|
||||||
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
|
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
|
||||||
case val
|
case val
|
||||||
when Regexp
|
when Regexp
|
||||||
xpath[XPath.attr(name)[regexp_to_conditions(val)]]
|
xpath[XPath.attr(name)[regexp_to_xpath_conditions(val)]]
|
||||||
when true
|
when true
|
||||||
xpath[XPath.attr(name)]
|
xpath[XPath.attr(name)]
|
||||||
when false
|
when false
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'xpath'
|
|
||||||
|
|
||||||
module Capybara
|
module Capybara
|
||||||
class Selector
|
class Selector
|
||||||
class RegexpDisassembler
|
class RegexpDisassembler
|
||||||
|
@ -10,14 +8,6 @@ module Capybara
|
||||||
@regexp_source = regexp.source
|
@regexp_source = regexp.source
|
||||||
end
|
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
|
def substrings
|
||||||
@substrings ||= begin
|
@substrings ||= begin
|
||||||
source = @regexp_source.dup
|
source = @regexp_source.dup
|
||||||
|
@ -37,7 +27,9 @@ module Capybara
|
||||||
end
|
end
|
||||||
return [] if source.include?('|') # can't handle alternation here
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -446,8 +446,12 @@ module Capybara
|
||||||
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
|
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
|
||||||
end
|
end
|
||||||
|
|
||||||
def regexp_to_conditions(regexp)
|
def regexp_to_xpath_conditions(regexp)
|
||||||
RegexpDisassembler.new(regexp).conditions
|
condition = XPath.current
|
||||||
|
condition = condition.uppercase if regexp.casefold?
|
||||||
|
RegexpDisassembler.new(regexp).substrings.map do |str|
|
||||||
|
condition.contains(str)
|
||||||
|
end.reduce(:&)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,22 @@ Capybara::SpecHelper.spec '#has_css?' do
|
||||||
expect(@session).not_to have_css('p.nosuchclass')
|
expect(@session).not_to have_css('p.nosuchclass')
|
||||||
end
|
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
|
it 'should respect scopes' do
|
||||||
@session.within "//p[@id='first']" do
|
@session.within "//p[@id='first']" do
|
||||||
expect(@session).to have_css('a#foo')
|
expect(@session).to have_css('a#foo')
|
||||||
|
|
|
@ -40,10 +40,20 @@ Capybara::SpecHelper.spec Capybara::Selector do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'field selectors' do
|
describe 'field selectors' do
|
||||||
|
context 'with :id option' do
|
||||||
it 'can find specifically by id' do
|
it 'can find specifically by id' do
|
||||||
expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com'
|
expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com'
|
||||||
end
|
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
|
it 'can find specifically by name' do
|
||||||
expect(@session.find(:field, name: 'form[other_title]')['id']).to eq 'form_other_title'
|
expect(@session.find(:field, name: 'form[other_title]')['id']).to eq 'form_other_title'
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</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>
|
<p>
|
||||||
<label for="form_other_title">Other title</label>
|
<label for="form_other_title">Other title</label>
|
||||||
<select name="form[other_title]" id="form_other_title">
|
<select name="form[other_title]" id="form_other_title">
|
||||||
|
|
|
@ -9,7 +9,12 @@ end
|
||||||
|
|
||||||
Capybara::SpecHelper.run_specs TestClass.new, 'DSL', capybara_skip: %i[
|
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
|
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
|
RSpec.describe Capybara::DSL do
|
||||||
after do
|
after do
|
||||||
|
|
|
@ -19,7 +19,12 @@ skipped_tests = %i[
|
||||||
download
|
download
|
||||||
css
|
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
|
RSpec.describe Capybara::Session do # rubocop:disable RSpec/MultipleDescribes
|
||||||
context 'with rack test driver' do
|
context 'with rack test driver' do
|
||||||
|
|
|
@ -148,6 +148,15 @@ RSpec.describe Capybara do
|
||||||
expect { string.find(:custom_css_selector, 'div', id: XPath.contains('peci')) }
|
expect { string.find(:custom_css_selector, 'div', id: XPath.contains('peci')) }
|
||||||
.to raise_error(ArgumentError, /not supported/)
|
.to raise_error(ArgumentError, /not supported/)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'with :class option' do
|
context 'with :class option' do
|
||||||
|
@ -177,6 +186,15 @@ RSpec.describe Capybara do
|
||||||
expect { string.find(:custom_css_selector, 'div', class: XPath.contains('random')) }
|
expect { string.find(:custom_css_selector, 'div', class: XPath.contains('random')) }
|
||||||
.to raise_error(ArgumentError, /not supported/)
|
.to raise_error(ArgumentError, /not supported/)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# :css, :xpath, :id, :field, :fieldset, :link, :button, :link_or_button, :fillable_field, :radio_button, :checkbox, :select,
|
# :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'
|
expect(string.find(:field, 'form[my_text_input]')[:id]).to eq 'my_text_input'
|
||||||
end
|
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]'
|
expect(string.find(:field, id: 'my_text_input')[:name]).to eq 'form[my_text_input]'
|
||||||
end
|
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
|
it 'finds by name' do
|
||||||
expect(string.find(:field, name: 'form[my_text_input]')[:id]).to eq 'my_text_input'
|
expect(string.find(:field, name: 'form[my_text_input]')[:id]).to eq 'my_text_input'
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue