Move :id and :class options to SelectorQuery and add some tests

This commit is contained in:
Thomas Walpole 2016-09-22 16:55:54 -07:00
parent 1592e4080e
commit b160533d5b
7 changed files with 160 additions and 29 deletions

View File

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

View File

@ -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<String>] :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<String>] :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<String>] :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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,9 @@
<p>
<label for="form_title">Title</label>
<select name="form[title]" id="form_title">
<option>Mrs</option>
<option>Mr</option>
<select name="form[title]" id="form_title" class="title">
<option class="title">Mrs</option>
<option class="title">Mr</option>
<option>Miss</option>
<option disabled="disabled">Other</option>
</select>

View File

@ -18,10 +18,13 @@ RSpec.describe Capybara do
<p class="b">Some Content</p>
<p class="b"></p>
</div>
<input type="checkbox"/>
<div id="#special">
</div>
<input id="2checkbox" class="2checkbox" type="checkbox"/>
<input type="radio"/>
<input type="text"/>
<input type="file"/>
<label for="my_text_input">My Text Input</label>
<input type="text" name="form[my_text_input]" placeholder="my text" id="my_text_input"/>
<input type="file" id="file" class=".special file"/>
<a href="#">link</a>
<fieldset></fieldset>
<select>
@ -42,6 +45,10 @@ RSpec.describe Capybara do
css { |css_class| "div.#{css_class}" }
filter(:not_empty, boolean: true, default: true, skip_if: :all) { |node, value| value ^ (node.text == '') }
end
Capybara.add_selector :custom_css_selector do
css { |selector| selector }
end
end
describe "modify_selector" do
@ -89,6 +96,75 @@ RSpec.describe Capybara do
end
end
context "with :id option", twtw: true do
it "works with compound css selectors" do
expect(string.all(:custom_css_selector, "div, h1", id: 'page').size).to eq 1
expect(string.all(:custom_css_selector, "h1, div", id: 'page').size).to eq 1
end
it "works with 'special' characters" do
expect(string.find(:custom_css_selector, "div", id: "#special")[:id]).to eq '#special'
expect(string.find(:custom_css_selector, "input", id: "2checkbox")[:id]).to eq '2checkbox'
end
end
context "with :class option", twtw: true do
it "works with compound css selectors" do
expect(string.all(:custom_css_selector, "div, h1", class: 'a').size).to eq 2
expect(string.all(:custom_css_selector, "h1, div", class: 'a').size).to eq 2
end
it "works with 'special' characters" do
expect(string.find(:custom_css_selector, "input", class: ".special")[:id]).to eq 'file'
expect(string.find(:custom_css_selector, "input", class: "2checkbox")[:id]).to eq '2checkbox'
end
end
# :css, :xpath, :id, :field, :fieldset, :link, :button, :link_or_button, :fillable_field, :radio_button, :checkbox, :select,
# :option, :file_field, :label, :table, :frame
describe ":css selector" do
it "finds by CSS locator" do
expect(string.find(:css, "input#my_text_input")[:name]).to eq 'form[my_text_input]'
end
end
describe ":xpath selector" do
it "finds by XPath locator" do
expect(string.find(:xpath, './/input[@id="my_text_input"]')[:name]).to eq 'form[my_text_input]'
end
end
describe ":id selector" do
it "finds by locator" do
expect(string.find(:id, "my_text_input")[:name]).to eq 'form[my_text_input]'
end
end
describe ":field selector" do
it "finds by locator" do
expect(string.find(:field, 'My Text Input')[:id]).to eq 'my_text_input'
expect(string.find(:field, 'my_text_input')[:id]).to eq 'my_text_input'
expect(string.find(:field, 'form[my_text_input]')[:id]).to eq 'my_text_input'
end
it "finds by id" do
expect(string.find(:field, id: 'my_text_input')[: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
it "finds by placeholder" do
expect(string.find(:field, placeholder: 'my text')[:id]).to eq 'my_text_input'
end
it "finds by type" do
expect(string.find(:field, type: 'file')[:id]).to eq 'file'
end
end
describe ":option selector" do
it "finds disabled options" do
expect(string.find(:option, disabled: true).value).to eq 'b'