mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Move :id and :class options to SelectorQuery and add some tests
This commit is contained in:
parent
1592e4080e
commit
b160533d5b
7 changed files with 160 additions and 29 deletions
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
30
lib/capybara/selector/css.rb
Normal file
30
lib/capybara/selector/css.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue