mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Merge pull request #1752 from jnicklas/class_selector
Add class as selectable attribute to some selectors
This commit is contained in:
commit
17479d7cd0
8 changed files with 175 additions and 37 deletions
|
@ -6,3 +6,8 @@ gemspec :path => '..'
|
|||
gem 'xpath', github: 'jnicklas/xpath'
|
||||
gem 'rack', github: 'rack/rack'
|
||||
gem 'sinatra', github: 'sinatra/sinatra', branch: 'master'
|
||||
gem 'rspec', github: 'rspec/rspec'
|
||||
gem 'rspec-core', github: 'rspec/rspec-core'
|
||||
gem 'rspec-support', github: 'rspec/rspec-support'
|
||||
gem 'rspec-mocks', github: 'rspec/rspec-mocks'
|
||||
gem 'rspec-expectations', github: 'rspec/rspec-expectations'
|
||||
|
|
|
@ -64,16 +64,18 @@ module Capybara
|
|||
#
|
||||
# page.fill_in 'Name', :with => 'Bob'
|
||||
#
|
||||
# @macro waiting_behavior
|
||||
#
|
||||
# @overload fill_in([locator], options={})
|
||||
# @param [String] locator Which field to fill in
|
||||
# @param [Hash] options
|
||||
# @macro waiting_behavior
|
||||
# @option options [String] :with The value to fill in - required
|
||||
# @option options [Hash] :fill_options Driver specific options regarding how to fill fields
|
||||
# @option options [Boolean] :multiple Match fields that can have multiple values?
|
||||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String] placeholder Match fields that match the placeholder attribute
|
||||
# @option options [String] :id Match fields that match the id attribute
|
||||
# @option options [String] :name Match fields that match the name attribute
|
||||
# @option options [String] :placeholder Match fields that match the placeholder attribute
|
||||
# @option options [String, Array<String>] :class Match links that match the class(es) provided
|
||||
#
|
||||
def fill_in(locator, options={})
|
||||
locator, options = nil, locator if locator.is_a? Hash
|
||||
|
@ -97,8 +99,9 @@ module Capybara
|
|||
# @param [String] locator Which radio button to choose
|
||||
#
|
||||
# @option options [String] :option Value of the radio_button to choose
|
||||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String] :id Match fields that match the id attribute
|
||||
# @option options [String] :name Match fields that match the name attribute
|
||||
# @option options [String, Array<String>] :class Match links that match the class(es) provided
|
||||
# @macro waiting_behavior
|
||||
# @macro label_click
|
||||
def choose(locator, options={})
|
||||
|
@ -133,6 +136,7 @@ module Capybara
|
|||
# @option options [String] :option Value of the checkbox to select
|
||||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String, Array<String>] :class Match links that match the class(es) provided
|
||||
# @macro label_click
|
||||
# @macro waiting_behavior
|
||||
#
|
||||
|
@ -168,6 +172,7 @@ module Capybara
|
|||
# @option options [String] :option Value of the checkbox to deselect
|
||||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String, Array<String>] :class Match links that match the class(es) provided
|
||||
# @macro label_click
|
||||
# @macro waiting_behavior
|
||||
#
|
||||
|
@ -253,6 +258,7 @@ module Capybara
|
|||
# @option options [Boolean] multiple Match field which allows multiple file selection
|
||||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String, Array<String>] :class Match links that match the class(es) provided
|
||||
#
|
||||
def attach_file(locator, path, options={})
|
||||
locator, path, options = nil, locator, path if path.is_a? Hash
|
||||
|
|
|
@ -70,6 +70,7 @@ module Capybara
|
|||
# @option options [String] id Match fields that match the id attribute
|
||||
# @option options [String] name Match fields that match the name attribute
|
||||
# @option options [String] placeholder Match fields that match the placeholder attribute
|
||||
# @option options [String, Array<String>] Match fields that match the class(es) passed
|
||||
# @return [Capybara::Node::Element] The found element
|
||||
#
|
||||
|
||||
|
@ -89,6 +90,10 @@ module Capybara
|
|||
# @macro waiting_behavior
|
||||
#
|
||||
# @option options [String,Regexp] href Value to match against the links href
|
||||
# @option options [String] id Match links with the id provided
|
||||
# @option options [String] title Match links with the title provided
|
||||
# @option options [String] alt Match links with a contained img element whose alt matches
|
||||
# @option options [String, Array<String>] class Match links that match the class(es) provided
|
||||
# @return [Capybara::Node::Element] The found element
|
||||
#
|
||||
def find_link(locator=nil, options={})
|
||||
|
@ -114,6 +119,10 @@ module Capybara
|
|||
# * true - only finds a disabled button
|
||||
# * false - only finds an enabled button
|
||||
# * :all - finds either an enabled or disabled button
|
||||
# @option options [String] id Match buttons with the id provided
|
||||
# @option options [String] title Match buttons with the title provided
|
||||
# @option options [String] value Match buttons with the value provided
|
||||
# @option options [String, Array<String>] class Match links that match the class(es) provided
|
||||
# @return [Capybara::Node::Element] The found element
|
||||
#
|
||||
def find_button(locator=nil, options={})
|
||||
|
|
|
@ -51,5 +51,7 @@ else
|
|||
RSpec.describe(*args, &block)
|
||||
end
|
||||
|
||||
RSpec.configuration.include Capybara::Features, :capybara_feature => true
|
||||
RSpec.configure do |config|
|
||||
config.include(Capybara::Features, :capybara_feature => true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ Capybara.add_selector(:id) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:field) do
|
||||
xpath(:id, :name, :placeholder, :type) do |locator, options|
|
||||
xpath(:id, :name, :placeholder, :type, :class) 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
|
||||
|
@ -42,7 +42,8 @@ Capybara.add_selector(:field) do
|
|||
xpath = xpath[XPath.attr(:type).equals(type)]
|
||||
end
|
||||
end
|
||||
locate_field(xpath, locator, options)
|
||||
xpath=locate_field(xpath, locator, options)
|
||||
xpath
|
||||
end
|
||||
|
||||
filter_set(:_field) # checked/unchecked/disabled/multiple
|
||||
|
@ -61,17 +62,18 @@ Capybara.add_selector(:field) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:fieldset) do
|
||||
xpath(:id, :legend) do |locator, options|
|
||||
xpath(:id, :legend, :class) 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
|
||||
|
||||
Capybara.add_selector(:link) do
|
||||
xpath(:id, :title, :alt) do |locator, options={}|
|
||||
xpath(:id, :title, :alt, :class) do |locator, options={}|
|
||||
xpath = XPath.descendant(:a)[XPath.attr(:href)]
|
||||
unless locator.nil?
|
||||
locator = locator.to_s
|
||||
|
@ -82,8 +84,7 @@ Capybara.add_selector(:link) do
|
|||
matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
|
||||
xpath = xpath[matchers]
|
||||
end
|
||||
xpath = xpath[XPath.attr(:id).equals(options[:id])] if options[:id]
|
||||
xpath = xpath[XPath.attr(:title).equals(options[:title])] if options[:title]
|
||||
xpath = [:id, :title, :class].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
|
||||
|
@ -100,7 +101,7 @@ Capybara.add_selector(:link) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:button) do
|
||||
xpath(:id, :value, :title) do |locator, options={}|
|
||||
xpath(:id, :value, :title, :class) 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')]
|
||||
|
@ -121,7 +122,7 @@ Capybara.add_selector(:button) do
|
|||
|
||||
res_xpath = input_btn_xpath + btn_xpath + image_btn_xpath
|
||||
|
||||
(expression_filters & options.keys).inject(res_xpath) { |xpath, ef| xpath[XPath.attr(ef).equals(options[ef])] }
|
||||
res_xpath = expression_filters.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
|
||||
res_xpath
|
||||
end
|
||||
|
@ -149,7 +150,7 @@ end
|
|||
|
||||
Capybara.add_selector(:fillable_field) do
|
||||
label "field"
|
||||
xpath(:id, :name, :placeholder) do |locator, options|
|
||||
xpath(:id, :name, :placeholder, :class) 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
|
||||
|
@ -165,7 +166,7 @@ end
|
|||
|
||||
Capybara.add_selector(:radio_button) do
|
||||
label "radio button"
|
||||
xpath(:id, :name) do |locator, options|
|
||||
xpath(:id, :name, :class) do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
@ -183,7 +184,7 @@ Capybara.add_selector(:radio_button) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:checkbox) do
|
||||
xpath(:id, :name) do |locator, options|
|
||||
xpath(:id, :name, :class) do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
@ -202,7 +203,7 @@ end
|
|||
|
||||
Capybara.add_selector(:select) do
|
||||
label "select box"
|
||||
xpath(:id, :name, :placeholder) do |locator, options|
|
||||
xpath(:id, :name, :placeholder, :class) do |locator, options|
|
||||
xpath = XPath.descendant(:select)
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
@ -259,7 +260,7 @@ end
|
|||
|
||||
Capybara.add_selector(:file_field) do
|
||||
label "file field"
|
||||
xpath(:id, :name) do |locator, options|
|
||||
xpath(:id, :name, :class) do |locator, options|
|
||||
xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
|
||||
locate_field(xpath, locator, options)
|
||||
end
|
||||
|
@ -301,11 +302,11 @@ Capybara.add_selector(:label) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:table) do
|
||||
xpath(:id, :caption) do |locator, options|
|
||||
xpath(:id, :caption, :class) 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.attr(:id).equals(options[:id])] if options[:id]
|
||||
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
|
||||
|
||||
|
@ -318,10 +319,10 @@ Capybara.add_selector(:table) do
|
|||
end
|
||||
|
||||
Capybara.add_selector(:frame) do
|
||||
xpath(:id, :name) do |locator, options|
|
||||
xpath(:id, :name, :class) 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?
|
||||
[:id, :name].each { |ef| xpath = xpath[XPath.attr(ef).equals(options[ef])] if options[ef] }
|
||||
xpath = expression_filters.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
|
||||
xpath
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
require 'capybara/selector/filter_set'
|
||||
require 'xpath'
|
||||
|
||||
#Patch XPath to allow a nil condition in where
|
||||
module XPath
|
||||
class Renderer
|
||||
def where(on, condition)
|
||||
condition = condition.to_s
|
||||
if !condition.empty?
|
||||
"#{on}[#{condition}]"
|
||||
else
|
||||
"#{on}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Capybara
|
||||
class Selector
|
||||
|
@ -33,6 +48,7 @@ module Capybara
|
|||
@description = nil
|
||||
@format = nil
|
||||
@expression = nil
|
||||
@expression_filters = []
|
||||
instance_eval(&block)
|
||||
end
|
||||
|
||||
|
@ -40,26 +56,79 @@ module Capybara
|
|||
@filter_set.filters
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Define a selector by an xpath expression
|
||||
#
|
||||
# @overload xpath(*expression_filters, &block)
|
||||
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this expression
|
||||
# @yield [locator, options] The block to use to generate the XPath expression
|
||||
# @yieldparam [String] locator The locator string passed to the query
|
||||
# @yieldparam [Hash] options The options hash passed to the query
|
||||
# @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
|
||||
#
|
||||
# @overload xpath()
|
||||
# @return [#call] The block that will be called to generate the XPath expression
|
||||
#
|
||||
def xpath(*expression_filters, &block)
|
||||
@format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
|
||||
format == :xpath ? @expression : nil
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Define a selector by a CSS selector
|
||||
#
|
||||
# @overload css(*expression_filters, &block)
|
||||
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
|
||||
# @yield [locator, options] The block to use to generate the CSS selector
|
||||
# @yieldparam [String] locator The locator string passed to the query
|
||||
# @yieldparam [Hash] options The options hash passed to the query
|
||||
# @yieldreturn [#to_s] An object that can produce a CSS selector
|
||||
#
|
||||
# @overload css()
|
||||
# @return [#call] The block that will be called to generate the CSS selector
|
||||
#
|
||||
def css(*expression_filters, &block)
|
||||
@format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
|
||||
format == :css ? @expression : nil
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Automatic selector detection
|
||||
#
|
||||
# @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
|
||||
# @yieldparam [String], locator The locator string used to determin if it matches the selector
|
||||
# @yieldreturn [Boolean] Whether this selector matches the locator string
|
||||
# @return [#call] The block that will be used to detect selector match
|
||||
#
|
||||
def match(&block)
|
||||
@match = block if block
|
||||
@match
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Set/get a descriptive label for the selector
|
||||
#
|
||||
# @overload label(label)
|
||||
# @param [String] label A descriptive label for this selector - used in error messages
|
||||
# @overload label()
|
||||
# @return [String] The currently set label
|
||||
#
|
||||
def label(label=nil)
|
||||
@label = label if label
|
||||
@label
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Description of the selector
|
||||
#
|
||||
# @param [Hash] options The options of the query used to generate the description
|
||||
# @return [String] Description of the selector when used with the options passed
|
||||
#
|
||||
def description(options={})
|
||||
@filter_set.description(options)
|
||||
end
|
||||
|
@ -73,10 +142,30 @@ module Capybara
|
|||
end
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Should this selector be used for the passed in locator
|
||||
#
|
||||
# This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
|
||||
#
|
||||
# @param [String] locator The locator passed to the query
|
||||
# @return [Boolean] Whether or not to use this selector
|
||||
#
|
||||
def match?(locator)
|
||||
@match and @match.call(locator)
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Define a non-expression filter for use with this selector
|
||||
# @overload filter(name, *types, options={}, &block)
|
||||
# @param [Symbol] name The filter name
|
||||
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
|
||||
# @param [Hash] options ({}) Options of the filter
|
||||
# @option options [Array<>] :valid_values Valid values for this filter
|
||||
# @option options :default The default value of the filter (if any)
|
||||
# @option options :skip_if Value of the filter that will cause it to be skipped
|
||||
#
|
||||
def filter(name, *types_and_options, &block)
|
||||
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
|
||||
types_and_options.each { |k| options[k] = true}
|
||||
|
@ -98,7 +187,7 @@ module Capybara
|
|||
private
|
||||
|
||||
def locate_field(xpath, locator, options={})
|
||||
locate_field = xpath
|
||||
locate_xpath = xpath #need to save original xpath for the label wrap
|
||||
if locator
|
||||
locator = locator.to_s
|
||||
attr_matchers = XPath.attr(:id).equals(locator) |
|
||||
|
@ -107,11 +196,31 @@ module Capybara
|
|||
XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
|
||||
attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
|
||||
|
||||
locate_field = locate_field[attr_matchers]
|
||||
locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
|
||||
locate_xpath = locate_xpath[attr_matchers]
|
||||
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
|
||||
end
|
||||
|
||||
def find_by_attr(attribute, value)
|
||||
finder_name = "find_by_#{attribute.to_s}_attr"
|
||||
if respond_to?(finder_name, true)
|
||||
send(finder_name, value)
|
||||
else
|
||||
value ? XPath.attr(attribute).equals(value) : nil
|
||||
end
|
||||
end
|
||||
|
||||
def find_by_class_attr(classes)
|
||||
if classes
|
||||
Array(classes).map do |klass|
|
||||
"contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
|
||||
end.join(" and ").to_sym
|
||||
else
|
||||
nil
|
||||
end
|
||||
[:id, :name, :placeholder].each { |ef| locate_field = locate_field[XPath.attr(ef).equals(options[ef])] if options[ef] }
|
||||
locate_field
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,5 +51,11 @@ Capybara::SpecHelper.spec Capybara::Selector do
|
|||
expect(@session.find(:field, 'Confusion', type: 'text')['id']).to eq 'confusion_text'
|
||||
expect(@session.find(:field, 'Confusion', type: 'textarea')['id']).to eq 'confusion_textarea'
|
||||
end
|
||||
|
||||
it "can find by class" do
|
||||
expect(@session.find(:field, class: 'confusion-checkbox')['id']).to eq 'confusion_checkbox'
|
||||
expect(@session).to have_selector(:field, class: 'confusion', count: 3)
|
||||
expect(@session.find(:field, class: ['confusion','confusion-textarea'])['id']).to eq 'confusion_textarea'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -527,14 +527,14 @@ New line after and before textarea tag
|
|||
</form>
|
||||
|
||||
<label>Confusion
|
||||
<input type="checkbox" id="confusion_checkbox"/>
|
||||
<input type="checkbox" id="confusion_checkbox" class="confusion-checkbox confusion"/>
|
||||
</label>
|
||||
|
||||
<label>Confusion
|
||||
<input type="text" id="confusion_text"/>
|
||||
<input type="text" id="confusion_text" class="confusion-text confusion"/>
|
||||
</label>
|
||||
|
||||
<label>Confusion
|
||||
<textarea id="confusion_textarea"/>
|
||||
<textarea id="confusion_textarea" class="confusion confusion-textarea"/>
|
||||
</label>
|
||||
|
||||
|
|
Loading…
Reference in a new issue