diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index 6f5978a3..a308aa0f 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -240,10 +240,15 @@ module Capybara end def filtered_css(expr) + id_conditions = conditions_from_id if use_default_id_filter? + id_conditions = [''] unless id_conditions&.any? + + class_conditions = conditions_from_classes if use_default_class_filter? + class_conditions = [''] unless class_conditions&.any? + + conditions = id_conditions.product(class_conditions) ::Capybara::Selector::CSS.split(expr).map do |sel| - sel += conditions_from_id if use_default_id_filter? - sel += conditions_from_classes if use_default_class_filter? - sel + conditions.map { |(id_cond, class_cond)| sel + id_cond + class_cond }.join(', ') end.join(', ') end @@ -260,7 +265,7 @@ module Capybara end def conditions_from_id - builder.attribute_conditions(id: options[:id]) + builder.id_conditions(options[:id]) end def apply_expression_filters(expression) diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index 713959bd..0b4ae611 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -34,7 +34,7 @@ Capybara.add_selector(:css) do end Capybara.add_selector(:id) do - xpath { |id| XPath.descendant[builder.attribute_conditions(id: id)] } + xpath { |id| XPath.descendant[builder.id_conditions(id)] } locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true } end diff --git a/lib/capybara/selector/builders/css_builder.rb b/lib/capybara/selector/builders/css_builder.rb index 9d612992..06d65f10 100644 --- a/lib/capybara/selector/builders/css_builder.rb +++ b/lib/capybara/selector/builders/css_builder.rb @@ -35,11 +35,28 @@ module Capybara when XPath::Expression raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors' when Regexp - attribute_conditions(class: classes) + Selector::RegexpDisassembler.new(classes).alternated_substrings.map do |strs| + strs.map do |str| + "[class*='#{str}'#{' i' if classes.casefold?}]" + end.join + end else cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') } - (cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } + - cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join + [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } + + cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join] + end + end + + def id_conditions(id) + case id + when Regexp + Selector::RegexpDisassembler.new(id).alternated_substrings.map do |id_strs| + id_strs.map do |str| + "[id*='#{str}'#{' i' if id.casefold?}]" + end.join + end + else + [attribute_conditions(id: id)] end end end diff --git a/lib/capybara/selector/builders/xpath_builder.rb b/lib/capybara/selector/builders/xpath_builder.rb index 60a6acb5..bf10e3a3 100644 --- a/lib/capybara/selector/builders/xpath_builder.rb +++ b/lib/capybara/selector/builders/xpath_builder.rb @@ -39,6 +39,10 @@ module Capybara end end + def id_conditions(id) + attribute_conditions(id: id) + end + private def regexp_to_xpath_conditions(regexp) diff --git a/lib/capybara/spec/session/has_css_spec.rb b/lib/capybara/spec/session/has_css_spec.rb index 70eea0f6..b0a6b38a 100644 --- a/lib/capybara/spec/session/has_css_spec.rb +++ b/lib/capybara/spec/session/has_css_spec.rb @@ -27,11 +27,15 @@ Capybara::SpecHelper.spec '#has_css?' do expect(@session).to have_css('h2', id: 'h2one') expect(@session).to have_css('h2') expect(@session).to have_css('h2', id: /h2o/) + expect(@session).to have_css('li', id: /john|paul/) end it 'should support :class option' do expect(@session).to have_css('li', class: 'guitarist') expect(@session).to have_css('li', class: /guitar/) + expect(@session).to have_css('li', class: /guitar|drummer/) + expect(@session).to have_css('li', class: %w[beatle guitarist]) + expect(@session).to have_css('li', class: /.*/) end it 'should support case insensitive :class and :id options' do @@ -111,6 +115,9 @@ Capybara::SpecHelper.spec '#has_css?' do expect(@session).to have_css('p', count: 3) expect(@session).to have_css('p a#foo', count: 1) expect(@session).to have_css('p a.doesnotexist', count: 0) + expect(@session).to have_css('li', class: /guitar|drummer/, count: 4) + expect(@session).to have_css('li', id: /john|paul/, class: /guitar|drummer/, count: 2) + expect(@session).to have_css('li', class: %w[beatle guitarist], count: 2) end it 'should be false if the content occurs a different number of times than the given' do diff --git a/lib/capybara/spec/session/has_xpath_spec.rb b/lib/capybara/spec/session/has_xpath_spec.rb index 16d9afa5..3332e175 100644 --- a/lib/capybara/spec/session/has_xpath_spec.rb +++ b/lib/capybara/spec/session/has_xpath_spec.rb @@ -11,6 +11,20 @@ Capybara::SpecHelper.spec '#has_xpath?' do expect(@session).to have_xpath("//p[contains(.,'est')]") end + it 'should support :id option' do + expect(@session).to have_xpath('//h2', id: 'h2one') + expect(@session).to have_xpath('//h2') + expect(@session).to have_xpath('//h2', id: /h2o/) + end + + it 'should support :class option' do + expect(@session).to have_xpath('//li', class: 'guitarist') + expect(@session).to have_xpath('//li', class: /guitar/) + expect(@session).to have_xpath('//li', class: /guitar|drummer/) + expect(@session).to have_xpath('//li', class: %w[beatle guitarist]) + expect(@session).to have_xpath('//li', class: /.*/) + end + it 'should be false if the given selector is not on the page' do expect(@session).not_to have_xpath('//abbr') expect(@session).not_to have_xpath("//p//a[@id='doesnotexist']") @@ -43,6 +57,9 @@ Capybara::SpecHelper.spec '#has_xpath?' do expect(@session).to have_xpath("//p//a[@id='foo']", count: 1) expect(@session).to have_xpath("//p[contains(.,'est')]", count: 1) expect(@session).to have_xpath("//p//a[@id='doesnotexist']", count: 0) + expect(@session).to have_xpath('//li', class: /guitar|drummer/, count: 4) + expect(@session).to have_xpath('//li', id: /john|paul/, class: /guitar|drummer/, count: 2) + expect(@session).to have_xpath('//li', class: %w[beatle guitarist], count: 2) end it 'should be false if the content occurs a different number of times than the given' do