lib/capybara/selector.rb in capybara-2.8.1 vs lib/capybara/selector.rb in capybara-2.9.0

- old
+ new

@@ -1,200 +1,148 @@ # frozen_string_literal: true -require 'capybara/selector/filter_set' +require 'capybara/selector/selector' +Capybara::Selector::FilterSet.add(:_field) do + filter(:checked, :boolean) { |node, value| not(value ^ node.checked?) } + filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) } + filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) } + filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) } -module Capybara - class Selector - - attr_reader :name, :format - - class << self - def all - @selectors ||= {} - end - - def add(name, &block) - all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block) - end - - def update(name, &block) - all[name.to_sym].instance_eval(&block) - end - - def remove(name) - all.delete(name.to_sym) - end - end - - def initialize(name, &block) - @name = name - @filter_set = FilterSet.add(name){} - @match = nil - @label = nil - @failure_message = nil - @description = nil - @format = nil - @expression = nil - instance_eval(&block) - end - - def custom_filters - @filter_set.filters - end - - def xpath(&block) - @format, @expression = :xpath, block if block - format == :xpath ? @expression : nil - end - - def css(&block) - @format, @expression = :css, block if block - format == :css ? @expression : nil - end - - def match(&block) - @match = block if block - @match - end - - def label(label=nil) - @label = label if label - @label - end - - def description(options={}) - @filter_set.description(options) - end - - def call(locator) - if format - @expression.call(locator) - else - warn "Selector has no format" - end - end - - def match?(locator) - @match and @match.call(locator) - end - - def filter(name, options={}, &block) - custom_filters[name] = Filter.new(name, block, options) - end - - def filter_set(name, filters_to_use = nil) - f_set = FilterSet.all[name] - f_set.filters.each do | name, filter | - custom_filters[name] = filter if filters_to_use.nil? || filters_to_use.include?(name) - end - f_set.descriptions.each { |desc| @filter_set.describe &desc } - end - - def describe &block - @filter_set.describe &block - end - - private - - def locate_field(xpath, locator) - attr_matchers = XPath.attr(:id).equals(locator) | - XPath.attr(:name).equals(locator) | - XPath.attr(:placeholder).equals(locator) | - 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 = xpath[attr_matchers] - locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath) - locate_field - end + describe do |options| + desc, states = String.new, [] + states << 'checked' if options[:checked] || (options[:unchecked] === false) + states << 'not checked' if options[:unchecked] || (options[:checked] === false) + states << 'disabled' if options[:disabled] == true + desc << " that is #{states.join(' and ')}" unless states.empty? + desc << " with the multiple attribute" if options[:multiple] == true + desc << " without the multiple attribute" if options[:multiple] === false + desc end end +## +# +# Select elements by XPath expression +# +# @locator An XPath expression +# Capybara.add_selector(:xpath) do xpath { |xpath| xpath } end +## +# +# Select elements by CSS selector +# +# @locator A CSS selector +# Capybara.add_selector(:css) do css { |css| css } end +## +# +# Select element by id +# +# @locator The id of the element to match +# Capybara.add_selector(:id) do xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] } end -Capybara::Selector::FilterSet.add(:_field) do - filter(:id) { |node, id| node['id'] == id } - filter(:name) { |node, name| node['name'] == name } - filter(:placeholder) { |node, placeholder| node['placeholder'] == placeholder } - filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) } - filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) } - filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } - filter(:multiple, boolean: true) { |node, value| !(value ^ node.multiple?) } - - describe do |options| - desc, states = String.new, [] - [:id, :name, :placeholder].each do |opt| - desc << " with #{opt.to_s} #{options[opt]}" if options.has_key?(opt) - end - states << 'checked' if options[:checked] || (options[:unchecked] === false) - states << 'not checked' if options[:unchecked] || (options[:checked] === false) - states << 'disabled' if options[:disabled] == true - desc << " that is #{states.join(' and ')}" unless states.empty? - desc << " with the multiple attribute" if options[:multiple] == true - desc << " without the multiple attribute" if options[:multiple] === false - desc - end -end - +## +# +# Select field elements (input [not of type submit, image, or hidden], textarea, select) +# +# @locator Matches against the id, name, or placeholder +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String] :placeholder Matches the placeholder attribute +# @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select' +# @filter [Boolean] :readonly +# @filter [String] :with Matches the current value of the field +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :checked Match checked fields? +# @filter [Boolean] :unchecked Match unchecked fields? +# @filter [Boolean] :disabled Match disabled field? +# @filter [Boolean] :multiple Match fields that accept multiple values Capybara.add_selector(:field) do - xpath do |locator| + xpath(:id, :name, :placeholder, :type, :class) do |locator, options| xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')] - xpath = locate_field(xpath, locator.to_s) unless locator.nil? + if options[:type] + type=options[:type].to_s + if ['textarea', 'select'].include?(type) + xpath = XPath.descendant(type.to_sym) + else + xpath = xpath[XPath.attr(:type).equals(type)] + end + end + xpath=locate_field(xpath, locator, options) xpath end - filter_set(:_field) + filter_set(:_field) # checked/unchecked/disabled/multiple - filter(:readonly, boolean: true) { |node, value| not(value ^ node.readonly?) } + filter(:readonly, :boolean) { |node, value| not(value ^ node.readonly?) } filter(:with) do |node, with| with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s end - filter(:type) do |node, type| - type = type.to_s - if ['textarea', 'select'].include?(type) - node.tag_name == type - else - node[:type] == type - end - end describe do |options| - desc, states = String.new, [] + desc = String.new + (expression_filters - [:type]).each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } desc << " of type #{options[:type].inspect}" if options[:type] desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with) desc end end +## +# +# Select fieldset elements +# +# @locator Matches id or contents of wrapped legend +# +# @filter [String] :id Matches id attribute +# @filter [String] :legend Matches contents of wrapped legend +# @filter [String, Array<String>] :class Matches the class(es) provided +# Capybara.add_selector(:fieldset) do - xpath do |locator| + 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 +## +# +# Find links ( <a> elements with an href attribute ) +# +# @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element +# +# @filter [String] :id Matches the id attribute +# @filter [String] :title Matches the title attribute +# @filter [String] :alt Matches the alt attribute of a contained img element +# @filter [String] :class Matches the class(es) provided +# @filter [String, Regexp] :href Matches the normalized href of the link +# Capybara.add_selector(:link) do - xpath do |locator| + xpath(:id, :title, :alt, :class) do |locator, options={}| xpath = XPath.descendant(:a)[XPath.attr(:href)] unless locator.nil? locator = locator.to_s matchers = XPath.attr(:id).equals(locator) | XPath.string.n.is(locator) | XPath.attr(:title).is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)] 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 = xpath[XPath.descendant(:img)[XPath.attr(:alt).equals(options[:alt])]] if options[:alt] xpath end filter(:href) do |node, href| if href.is_a? Regexp @@ -205,12 +153,23 @@ end describe { |options| " with href #{options[:href].inspect}" if options[:href] } end +## +# +# Find buttons ( input [of type submit, reset, image, button] or button elements ) +# +# @locator Matches the id, value, or title attributes, string content of a button, or the alt attribute of an image type button +# +# @filter [String] :id Matches the id attribute +# @filter [String] :title Matches the title attribute +# @filter [String] :class Matches the class(es) provided +# @filter [String] :value Matches the value of an input button +# Capybara.add_selector(:button) do - xpath do |locator| + 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')] unless locator.nil? @@ -225,86 +184,162 @@ alt_matches = XPath.attr(:alt).is(locator) alt_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label image_btn_xpath = image_btn_xpath[alt_matches] end - input_btn_xpath + btn_xpath + image_btn_xpath + res_xpath = input_btn_xpath + btn_xpath + image_btn_xpath + + res_xpath = expression_filters.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] } + + res_xpath end - filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } + filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) } - describe { |options| " that is disabled" if options[:disabled] == true } + describe do |options| + desc = String.new + desc << " that is disabled" if options[:disabled] == true + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } + desc + end end +## +# +# Find links or buttons +# Capybara.add_selector(:link_or_button) do label "link or button" - xpath do |locator| - self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator)}.reduce(:+) + xpath do |locator, options| + self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator, options)}.reduce(:+) end - filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) } + filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) } describe { |options| " that is disabled" if options[:disabled] } end +## +# +# Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] ) +# +# @locator Matches against the id, name, or placeholder +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String] :placeholder Matches the placeholder attribute +# @filter [String] :with Matches the current value of the field +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :disabled Match disabled field? +# @filter [Boolean] :multiple Match fields that accept multiple values +# Capybara.add_selector(:fillable_field) do label "field" - xpath do |locator| + xpath(:id, :name, :placeholder, :class) do |locator, options| xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')] - xpath = locate_field(xpath, locator.to_s) unless locator.nil? - xpath + locate_field(xpath, locator, options) end - filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple]) + filter_set(:_field, [:disabled, :multiple]) + + filter(:with) do |node, with| + with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s + end + + describe do |options| + desc = String.new + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } + desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with) + desc + end end +## +# +# Find radio buttons +# +# @locator Match id, name, or associated label text +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :checked Match checked fields? +# @filter [Boolean] :unchecked Match unchecked fields? +# @filter [Boolean] :disabled Match disabled field? +# @filter [String] :option Match the value +# Capybara.add_selector(:radio_button) do label "radio button" - xpath do |locator| + xpath(:id, :name, :class) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')] - xpath = locate_field(xpath, locator.to_s) unless locator.nil? - xpath + locate_field(xpath, locator, options) end - filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled]) + filter_set(:_field, [:checked, :unchecked, :disabled]) filter(:option) { |node, value| node.value == value.to_s } describe do |options| desc = String.new desc << " with value #{options[:option].inspect}" if options[:option] + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } desc end end +## +# +# Find checkboxes +# +# @locator Match id, name, or associated label text +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :checked Match checked fields? +# @filter [Boolean] :unchecked Match unchecked fields? +# @filter [Boolean] :disabled Match disabled field? +# @filter [String] :option Match the value +# Capybara.add_selector(:checkbox) do - xpath do |locator| + xpath(:id, :name, :class) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')] - xpath = locate_field(xpath, locator.to_s) unless locator.nil? - xpath + locate_field(xpath, locator, options) end - filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled]) + filter_set(:_field, [:checked, :unchecked, :disabled]) filter(:option) { |node, value| node.value == value.to_s } describe do |options| desc = String.new desc << " with value #{options[:option].inspect}" if options[:option] + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } desc end end +## +# +# Find select elements +# +# @locator Match id, name, placeholder, or associated label text +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String] :placeholder Matches the placeholder attribute +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :disabled Match disabled field? +# @filter [Boolean] :multiple Match fields that accept multiple values +# @filter [Array<String>] :options Exact match options +# @filter [Array<String>] :with_options Partial match options +# @filter [String, Array<String>] :selected Match the selection(s) +# Capybara.add_selector(:select) do label "select box" - xpath do |locator| + xpath(:id, :name, :placeholder, :class) do |locator, options| xpath = XPath.descendant(:select) - xpath = locate_field(xpath, locator.to_s) unless locator.nil? - xpath + locate_field(xpath, locator, options) end - filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple]) + filter_set(:_field, [:disabled, :multiple]) filter(:options) do |node, options| if node.visible? actual = node.all(:xpath, './/option').map { |option| option.text } else @@ -327,43 +362,75 @@ describe do |options| desc = String.new desc << " with options #{options[:options].inspect}" if options[:options] desc << " with at least options #{options[:with_options].inspect}" if options[:with_options] desc << " with #{options[:selected].inspect} selected" if options[:selected] + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } desc end end +## +# +# Find option elements +# +# @locator Match text of option +# @filter [Boolean] :disabled Match disabled option +# @filter [Boolean] :selected Match selected option +# Capybara.add_selector(:option) do xpath do |locator| xpath = XPath.descendant(:option) xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil? xpath end - filter(:disabled, boolean: true) { |node, value| not(value ^ node.disabled?) } - filter(:selected, boolean: true) { |node, value| not(value ^ node.selected?) } + filter(:disabled, :boolean) { |node, value| not(value ^ node.disabled?) } + filter(:selected, :boolean) { |node, value| not(value ^ node.selected?) } describe do |options| desc = String.new desc << " that is#{' not' unless options[:disabled]} disabled" if options.has_key?(:disabled) desc << " that is#{' not' unless options[:selected]} selected" if options.has_key?(:selected) desc end end +## +# +# Find file input elements +# +# @locator Match id, name, or associated label text +# @filter [String] :id Matches the id attribute +# @filter [String] :name Matches the name attribute +# @filter [String, Array<String>] :class Matches the class(es) provided +# @filter [Boolean] :disabled Match disabled field? +# @filter [Boolean] :multiple Match field that accepts multiple values +# Capybara.add_selector(:file_field) do label "file field" - xpath do |locator| + xpath(:id, :name, :class) do |locator, options| xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')] - xpath = locate_field(xpath, locator.to_s) unless locator.nil? - xpath + locate_field(xpath, locator, options) end - filter_set(:_field, [:id, :name, :disabled, :multiple]) + filter_set(:_field, [:disabled, :multiple]) + + describe do |options| + desc = String.new + expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) } + desc + end end +## +# +# Find label elements +# +# @locator Match id or text contents +# @filter [Element, String] :for The element or id of the element associated with the label +# Capybara.add_selector(:label) do label "label" xpath do |locator| xpath = XPath.descendant(:label) xpath = xpath[XPath.string.n.is(locator.to_s) | XPath.attr(:id).equals(locator.to_s)] unless locator.nil? @@ -387,20 +454,55 @@ desc << " for #{options[:for]}" if options[:for] desc end end +## +# +# Find table elements +# +# @locator id or caption text of table +# @filter [String] :id Match id attribute of table +# @filter [String] :caption Match text of associated caption +# @filter [String, Array<String>] :class Matches the class(es) provided +# Capybara.add_selector(:table) do - xpath do |locator| + 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.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 end +## +# +# Find frame/iframe elements +# +# @locator Match id or name +# @filter [String] :id Match id attribute +# @filter [String] :name Match name attribute +# @filter [String, Array<String>] :class Matches the class(es) provided +# Capybara.add_selector(:frame) do - xpath do |locator| + 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? + xpath = expression_filters.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 name #{options[:name]}" if options[:name] + desc end end