mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Support regexp for system :id and :class filters
This commit is contained in:
parent
15ca8c7d6a
commit
3778898a96
11 changed files with 149 additions and 46 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue