diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index 20c79aee..fef8070b 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -4,7 +4,7 @@ module Capybara class SelectorQuery < Queries::BaseQuery attr_accessor :selector, :locator, :options, :expression, :find, :negative - VALID_KEYS = COUNT_KEYS + [:text, :visible, :exact, :match, :wait, :filter_set] + VALID_KEYS = COUNT_KEYS + [:text, :id, :class, :visible, :exact, :match, :wait, :filter_set] VALID_MATCH = [:first, :smart, :prefer_exact, :one] def initialize(*args) @@ -39,6 +39,8 @@ module Capybara def description @description = String.new("#{label} #{locator.inspect}") @description << " with text #{options[:text].inspect}" if options[:text] + @description << " with id #{options[:id]}" if options[:id] + @description << " with classes #{Array(options[:class]).join(',')}]" if options[:class] @description << selector.description(options) @description end @@ -92,15 +94,16 @@ module Capybara def xpath(exact=nil) exact = self.exact? if exact.nil? - if @expression.respond_to?(:to_xpath) and exact + expr = if @expression.respond_to?(:to_xpath) and exact @expression.to_xpath(:exact) else @expression.to_s end + filtered_xpath(expr) end def css - @expression + filtered_css(@expression) end # @api private @@ -141,7 +144,7 @@ module Capybara end def custom_keys - query_filters.keys + @selector.expression_filters + @custom_keys ||= query_filters.keys + @selector.expression_filters end def assert_valid_keys @@ -151,6 +154,32 @@ module Capybara end end + def filtered_xpath(expr) + if options.has_key?(:id) || options.has_key?(:class) + expr = "(#{expr})" + expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.has_key?(:id) && !custom_keys.include?(:id) + if options.has_key?(:class) && !custom_keys.include?(:class) + class_xpath = Array(options[:class]).map do |klass| + "contains(concat(' ',normalize-space(@class),' '),' #{klass} ')" + end.join(" and ") + expr = "#{expr}[#{class_xpath}]" + end + end + expr + end + + def filtered_css(expr) + if options.has_key?(:id) || options.has_key?(:class) + css_selectors = expr.split(',').map(&:rstrip) + expr = css_selectors.map do |sel| + sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.has_key?(:id) && !custom_keys.include?(:id) + sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}"}.join if options.has_key?(:class) && !custom_keys.include?(:class) + sel + end.join(", ") + end + expr + end + def warn_exact_usage if options.has_key?(:exact) && !supports_exact? warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression.to_s}\" has no effect." diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index b0b72d5c..37233433 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -65,7 +65,7 @@ end # @filter [Boolean] :disabled Match disabled field? # @filter [Boolean] :multiple Match fields that accept multiple values Capybara.add_selector(:field) do - xpath(:id, :name, :placeholder, :type, :class) do |locator, options| + xpath(:name, :placeholder, :type) do |locator, options| xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')] if options[:type] type=options[:type].to_s @@ -105,12 +105,10 @@ end # @filter [String, Array] :class Matches the class(es) provided # Capybara.add_selector(:fieldset) do - xpath(:id, :legend, :class) do |locator, options| + xpath(:legend) do |locator, options| xpath = XPath.descendant(:fieldset) xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil? - xpath = xpath[XPath.attr(:id).equals(options[:id])] if options[:id] xpath = xpath[XPath.child(:legend)[XPath.string.n.is(options[:legend])]] if options[:legend] - xpath = xpath[find_by_attr(:class, options[:class])] xpath end end @@ -128,7 +126,7 @@ end # @filter [String, Regexp] :href Matches the normalized href of the link # Capybara.add_selector(:link) do - xpath(:id, :title, :alt, :class) do |locator, options={}| + xpath(:title, :alt) do |locator, options={}| xpath = XPath.descendant(:a)[XPath.attr(:href)] unless locator.nil? locator = locator.to_s @@ -139,7 +137,7 @@ Capybara.add_selector(:link) do matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label xpath = xpath[matchers] end - xpath = [:id, :title, :class].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } + xpath = [:title].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt).equals(options[:alt])]] if options[:alt] xpath end @@ -167,7 +165,7 @@ end # @filter [String] :value Matches the value of an input button # Capybara.add_selector(:button) do - xpath(:id, :value, :title, :class) do |locator, options={}| + xpath(:value, :title) do |locator, options={}| input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')] btn_xpath = XPath.descendant(:button) image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')] @@ -233,7 +231,7 @@ end # Capybara.add_selector(:fillable_field) do label "field" - xpath(:id, :name, :placeholder, :class) do |locator, options| + xpath(:name, :placeholder) do |locator, options| xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')] locate_field(xpath, locator, options) end @@ -267,7 +265,7 @@ end # Capybara.add_selector(:radio_button) do label "radio button" - xpath(:id, :name, :class) do |locator, options| + xpath(:name) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')] locate_field(xpath, locator, options) end @@ -298,7 +296,7 @@ end # @filter [String] :option Match the value # Capybara.add_selector(:checkbox) do - xpath(:id, :name, :class) do |locator, options| + xpath(:name) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')] locate_field(xpath, locator, options) end @@ -332,7 +330,7 @@ end # Capybara.add_selector(:select) do label "select box" - xpath(:id, :name, :placeholder, :class) do |locator, options| + xpath(:name, :placeholder) do |locator, options| xpath = XPath.descendant(:select) locate_field(xpath, locator, options) end @@ -408,7 +406,7 @@ end # Capybara.add_selector(:file_field) do label "file field" - xpath(:id, :name, :class) do |locator, options| + xpath(:name) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')] locate_field(xpath, locator, options) end @@ -466,17 +464,15 @@ end # @filter [String, Array] :class Matches the class(es) provided # Capybara.add_selector(:table) do - xpath(:id, :caption, :class) do |locator, options| + xpath(:caption) do |locator, options| xpath = XPath.descendant(:table) xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil? xpath = xpath[XPath.descendant(:caption).equals(options[:caption])] if options[:caption] - xpath = [:id, :class].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } xpath end describe do |options| desc = String.new - desc << " with id #{options[:id]}" if options[:id] desc << " with caption #{options[:caption]}" if options[:caption] desc end @@ -492,7 +488,7 @@ end # @filter [String, Array] :class Matches the class(es) provided # Capybara.add_selector(:frame) do - xpath(:id, :name, :class) do |locator, options| + xpath(:name) do |locator, options| xpath = XPath.descendant(:iframe) + XPath.descendant(:frame) xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.attr(:name).equals(locator)] unless locator.nil? xpath = expression_filters.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } @@ -501,7 +497,6 @@ Capybara.add_selector(:frame) do describe do |options| desc = String.new - desc << " with id #{options[:id]}" if options[:id] desc << " with name #{options[:name]}" if options[:name] desc end diff --git a/lib/capybara/selector/css.rb b/lib/capybara/selector/css.rb new file mode 100644 index 00000000..ca27f93f --- /dev/null +++ b/lib/capybara/selector/css.rb @@ -0,0 +1,30 @@ +module Capybara + class Selector + class CSS + def self.escape(str) + out = String.new("") + value = str.dup + out << value.slice!(0...1) if value =~ /^[-_]/ + out << if value[0] =~ NMSTART + value.slice!(0...1) + else + escape_char(value.slice!(0...1)) + end + out << value.gsub(/[^a-zA-Z0-9_-]/) {|c| escape_char c} + out + end + + def self.escape_char(c) + return "\\%06x" % c.ord() unless c =~ %r{[ -/:-~]} + "\\#{c}" + end + + S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}' + H = /[0-9a-fA-F]/ + UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/ + NONASCII = /[#{S}]/ + ESCAPE = /#{UNICODE}|\\[ -~#{S}]/ + NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/ + end + end +end \ No newline at end of file diff --git a/lib/capybara/selector/selector.rb b/lib/capybara/selector/selector.rb index c3379183..5e44c651 100644 --- a/lib/capybara/selector/selector.rb +++ b/lib/capybara/selector/selector.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require 'capybara/selector/filter_set' +require 'capybara/selector/css' require 'xpath' #Patch XPath to allow a nil condition in where @@ -201,7 +202,7 @@ module Capybara locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath) end - locate_xpath = [:id, :name, :placeholder, :class].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } + locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } locate_xpath end diff --git a/lib/capybara/spec/session/selectors_spec.rb b/lib/capybara/spec/session/selectors_spec.rb index a896067b..64f4edfe 100644 --- a/lib/capybara/spec/session/selectors_spec.rb +++ b/lib/capybara/spec/session/selectors_spec.rb @@ -33,7 +33,7 @@ Capybara::SpecHelper.spec Capybara::Selector do end end - describe "locate_field selectors" do + describe "field selectors" do it "can find specifically by id" do expect(@session.find(:field, id: 'customer_email').value).to eq "ben@ben.com" end diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb index 942524bf..eabd95fc 100644 --- a/lib/capybara/spec/views/form.erb +++ b/lib/capybara/spec/views/form.erb @@ -5,9 +5,9 @@

- + + diff --git a/spec/selector_spec.rb b/spec/selector_spec.rb index afd0de8a..e72436a2 100644 --- a/spec/selector_spec.rb +++ b/spec/selector_spec.rb @@ -18,10 +18,13 @@ RSpec.describe Capybara do

Some Content

- +
+
+ - - + + + link